diff --git a/.gitignore b/.gitignore index 2863ec0cb7..d815ed2081 100644 --- a/.gitignore +++ b/.gitignore @@ -20,9 +20,6 @@ closed-lib # Node dependencies node_modules -# Build documentation -docs - # Protractor logs protractor/logs diff --git a/README.md b/README.md index c36cbea653..42cd060282 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ as described above. An example of this is expressed in `platform/framework`, which follows bundle conventions. -### Regression Testing +### Functional Testing The tests described above are all at the unit-level; an additional test suite using [Protractor](https://angular.github.io/protractor/) @@ -76,9 +76,9 @@ us under development, in the `protractor` folder. To run: * Install protractor following the instructions above. -* `webdriver-manager start` -* `node app.js -p 1984 -x platform/persistence/elastic -i example/persistence -* `protractor protractor/conf.js` +* `cd protractor` +* `npm install` +* `npm run all` ## Build diff --git a/build-docs.sh b/build-docs.sh index dd62c5ec41..c318a0dbda 100755 --- a/build-docs.sh +++ b/build-docs.sh @@ -24,7 +24,7 @@ # Script to build and deploy docs to github pages. -OUTPUT_DIRECTORY="docs" +OUTPUT_DIRECTORY="target/docs" REPOSITORY_URL="git@github.com:nasa/openmctweb.git" BUILD_SHA=`git rev-parse head` @@ -39,7 +39,7 @@ if [ -d $OUTPUT_DIRECTORY ]; then rm -rf $OUTPUT_DIRECTORY || exit 1 fi -npm run-script jsdoc +npm run docs cd $OUTPUT_DIRECTORY || exit 1 echo "git init" diff --git a/bundles.json b/bundles.json index 0b97f1abab..898ca3d738 100644 --- a/bundles.json +++ b/bundles.json @@ -21,6 +21,7 @@ "platform/persistence/queue", "platform/policy", "platform/entanglement", + "platform/search", "example/imagery", "example/persistence", diff --git a/circle.yml b/circle.yml index 3ee4c7f1c3..2b86cc7b71 100644 --- a/circle.yml +++ b/circle.yml @@ -11,4 +11,4 @@ deployment: openmctweb-staging-deux: branch: mobile heroku: - appname: openmctweb-staging-deux \ No newline at end of file + appname: openmctweb-staging-deux diff --git a/docs/gendocs.js b/docs/gendocs.js new file mode 100644 index 0000000000..2fcda7214e --- /dev/null +++ b/docs/gendocs.js @@ -0,0 +1,193 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/*global require,process,GLOBAL*/ +/*jslint nomen: false */ + + +// Usage: +// node gendocs.js --in --out + +var CONSTANTS = { + DIAGRAM_WIDTH: 800, + DIAGRAM_HEIGHT: 500 + }; + +GLOBAL.window = GLOBAL.window || GLOBAL; // nomnoml expects window to be defined +(function () { + "use strict"; + + var fs = require("fs"), + mkdirp = require("mkdirp"), + path = require("path"), + glob = require("glob"), + marked = require("marked"), + split = require("split"), + stream = require("stream"), + nomnoml = require('nomnoml'), + Canvas = require('canvas'), + options = require("minimist")(process.argv.slice(2)); + + // Convert from nomnoml source to a target PNG file. + function renderNomnoml(source, target) { + var canvas = + new Canvas(CONSTANTS.DIAGRAM_WIDTH, CONSTANTS.DIAGRAM_HEIGHT); + nomnoml.draw(canvas, source, 1.0); + canvas.pngStream().pipe(fs.createWriteStream(target)); + } + + // Stream transform. + // Pulls out nomnoml diagrams from fenced code blocks and renders them + // as PNG files in the output directory, prefixed with a provided name. + // The fenced code blocks will be replaced with Markdown in the + // output of this stream. + function nomnomlifier(outputDirectory, prefix) { + var transform = new stream.Transform({ objectMode: true }), + isBuilding = false, + counter = 1, + outputPath, + source = ""; + + transform._transform = function (chunk, encoding, done) { + if (!isBuilding) { + if (chunk.trim().indexOf("```nomnoml") === 0) { + var outputFilename = prefix + '-' + counter + '.png'; + outputPath = path.join(outputDirectory, outputFilename); + this.push([ + "\n![Diagram ", + counter, + "](", + outputFilename, + ")\n\n" + ].join("")); + isBuilding = true; + source = ""; + counter += 1; + } else { + // Otherwise, pass through + this.push(chunk + '\n'); + } + } else { + if (chunk.trim() === "```") { + // End nomnoml + renderNomnoml(source, outputPath); + isBuilding = false; + } else { + source += chunk + '\n'; + } + } + done(); + }; + + return transform; + } + + // Convert from Github-flavored Markdown to HTML + function gfmifier() { + var transform = new stream.Transform({ objectMode: true }), + markdown = ""; + transform._transform = function (chunk, encoding, done) { + markdown += chunk; + done(); + }; + transform._flush = function (done) { + this.push("\n"); + this.push(marked(markdown)); + this.push("\n\n"); + done(); + }; + return transform; + } + + // Custom renderer for marked; converts relative links from md to html, + // and makes headings linkable. + function CustomRenderer() { + var renderer = new marked.Renderer(), + customRenderer = Object.create(renderer); + customRenderer.heading = function (text, level) { + var escapedText = (text || "").trim().toLowerCase().replace(/\W/g, "-"), + aOpen = "", + aClose = ""; + return aOpen + renderer.heading.apply(renderer, arguments) + aClose; + }; + // Change links to .md files to .html + customRenderer.link = function (href, title, text) { + // ...but only if they look like relative paths + return (href || "").indexOf(":") === -1 && href[0] !== "/" ? + renderer.link(href.replace(/\.md/, ".html"), title, text) : + renderer.link.apply(renderer, arguments); + }; + return customRenderer; + } + + options['in'] = options['in'] || options.i; + options.out = options.out || options.o; + + marked.setOptions({ + renderer: new CustomRenderer(), + gfm: true, + tables: true, + breaks: false, + pedantic: false, + sanitize: true, + smartLists: true, + smartypants: false + }); + + // Convert all markdown files. + // First, pull out nomnoml diagrams. + // Then, convert remaining Markdown to HTML. + glob(options['in'] + "/**/*.md", {}, function (err, files) { + files.forEach(function (file) { + var destination = file.replace(options['in'], options.out) + .replace(/md$/, "html"), + destPath = path.dirname(destination), + prefix = path.basename(destination).replace(/\.html$/, ""); + + mkdirp(destPath, function (err) { + fs.createReadStream(file, { encoding: 'utf8' }) + .pipe(split()) + .pipe(nomnomlifier(destPath, prefix)) + .pipe(gfmifier()) + .pipe(fs.createWriteStream(destination, { + encoding: 'utf8' + })); + }); + }); + }); + + // Also copy over all HTML, CSS, or PNG files + glob(options['in'] + "/**/*.@(html|css|png)", {}, function (err, files) { + files.forEach(function (file) { + var destination = file.replace(options['in'], options.out), + destPath = path.dirname(destination); + + mkdirp(destPath, function (err) { + fs.createReadStream(file, { encoding: 'utf8' }) + .pipe(fs.createWriteStream(destination, { + encoding: 'utf8' + })); + }); + }); + }); + +}()); diff --git a/docs/src/architecture/Framework.md b/docs/src/architecture/Framework.md new file mode 100644 index 0000000000..229bee39c4 --- /dev/null +++ b/docs/src/architecture/Framework.md @@ -0,0 +1,232 @@ +# Overview + +The framework layer's most basic responsibility is allowing individual +software components to communicate. The software components it recognizes +are: + +* _Extensions_: Individual units of functionality that can be added to + or removed from Open MCT Web. _Extension categories_ distinguish what + type of functionality is being added/removed. +* _Bundles_: A grouping of related extensions + (named after an analogous concept from [OSGi](http://www.osgi.org/)) + that may be added or removed as a group. + +The framework layer operates by taking a set of active bundles, and +exposing extensions to one another as-needed, using +[dependency injection](https://en.wikipedia.org/wiki/Dependency_injection). +Extensions are responsible for declaring their dependencies in a +manner which the framework layer can understand. + +```nomnoml +#direction: down +[Open MCT Web| + [Dependency injection framework]-->[Platform bundle #1] + [Dependency injection framework]-->[Platform bundle #2] + [Dependency injection framework]-->[Plugin bundle #1] + [Dependency injection framework]-->[Plugin bundle #2] + [Platform bundle #1|[Extensions]] + [Platform bundle #2|[Extensions]] + [Plugin bundle #1|[Extensions]] + [Plugin bundle #2|[Extensions]] + [Platform bundle #1]<->[Platform bundle #2] + [Plugin bundle #1]<->[Platform bundle #2] + [Plugin bundle #1]<->[Plugin bundle #2] +] +``` + +The "dependency injection framework" in this case is +[AngularJS](https://angularjs.org/). Open MCT Web's framework layer +is really just a thin wrapper over Angular that recognizes the +concepts of bundles and extensions (as declared in JSON files) and +registering extensions with Angular. It additionally acts as a +mediator between Angular and [RequireJS](http://requirejs.org/), +which is used to load JavaScript sources which implement +extensions. + +```nomnoml +[Framework layer| + [AngularJS]<-[Framework Component] + [RequireJS]<-[Framework Component] + [Framework Component]1o-*[Bundles] +] +``` + +It is worth noting that _no other components_ are "aware" of the +framework component directly; Angular and Require are _used by_ the +framework components, and extensions in various bundles will have +their dependencies satisfied by Angular as a consequence of registration +activities which were performed by the framework component. + + +## Application Initialization + +The framework component initializes an Open MCT Web application following +a simple sequence of steps. + +```nomnoml +[ Start]->[ Load bundles.json] +[Load bundles.json]->[ Load bundle.json files] +[Load bundle.json files]->[ Resolve implementations] +[Resolve implementations]->[ Register with Angular] +[Register with Angular]->[ Bootstrap application] +[Bootstrap application]->[ End] +``` + +1. __Loading bundles.json.__ A file named `bundles.json` is loaded to determine + which bundles to load. Bundles are given in this file as relative paths + which point to bundle directories. +2. __Load bundle.json files.__ Individual bundle definitions are loaded; a + `bundle.json` file is expected in each bundle directory. +2. __Resolving implementations.__ Any scripts which provide implementations for + extensions exposed by bundles are loaded, using RequireJS. +3. __Register with Angular.__ Resolved extensions are registered with Angular, + such that they can be used by the application at run-time. This stage + includes both registration of Angular built-ins (directives, controllers, + routes, constants, and services) as well as registration of non-Angular + extensions. +4. __Bootstrap application.__ Once all extensions have been registered, + the Angular application + [is bootstrapped](https://docs.angularjs.org/guide/bootstrap). + +## Architectural Paradigm + +```nomnoml +[Extension] +[Extension]o->[Dependency #1] +[Extension]o->[Dependency #2] +[Extension]o->[Dependency #3] +``` + +Open MCT Web's architecture relies on a simple premise: Individual units +(extensions) only have access to the dependencies they declare that they +need, and they acquire references to these dependencies via dependency +injection. This has several desirable traits: + +* Programming to an interface is enforced. Any given dependency can be + swapped out for something which exposes an equivalent interface. This + improves flexibility against refactoring, simplifies testing, and + provides a common mechanism for extension and reconfiguration. +* The dependencies of a unit must be explicitly defined. This means that + it can be easily determined what a given unit's role is within the + larger system, in terms of what other components it will interact with. + It also helps to enforce good separation of concerns: When a set of + declared dependencies becomes long it is obvious, and this is usually + a sign that a given unit is involved in too many concerns and should + be refactored into smaller pieces. +* Individual units do not need to be aware of the framework; they need + only be aware of the interfaces to the components they specifically + use. This avoids introducing a ubiquitous dependency upon the framework + layer itself; it is plausible to modify or replace the framework + without making changes to individual software components which run upon + the framework. + +A drawback to this approach is that it makes it difficult to define +"the architecture" of Open MCT Web, in terms of describing the specific +units that interact at run-time. The run-time architecture is determined +by the framework as the consequence of wiring together dependencies. +As such, the specific architecture of any given application built on +Open MCT Web can look very different. + +Keeping that in mind, there are a few useful patterns supported by the +framework that are useful to keep in mind. + +The specific service infrastructure provided by the platform is described +in the [Platform Architecture](Platform.md). + +## Extension Categories + +One of the capabilities that the framework component layers on top of +AngularJS is support for many-to-one dependencies. That is, a specific +extension may declare a dependency to _all extensions of a specific +category_, instead of being limited to declaring specific dependencies. + +```nomnoml +#direction: right +[Specific Extension] 1 o-> * [Extension of Some Category] +``` + +This is useful for introducing specific extension points to an application. +Some unit of software will depend upon all extensions of a given category +and integrate their behavior into the system in some fashion; plugin authors +can then add new extensions of that category to augment existing behaviors. + +Some developers may be familiar with the use of registries to achieve +similar characteristics. This approach is similar, except that the registry +is effectively implicit whenever a new extension category is used or +depended-upon. This has some advantages over a more straightforward +registry-based approach: + +* These many-to-one relationships are expressed as dependencies; an + extension category is registered as having dependencies on all individual + extensions of this category. This avoids ordering issues that may occur + with more conventional registries, which may be observed before all + dependencies are resolved. +* The need for service registries of specific types is removed, reducing + the number of interfaces to manage within the system. Groups of + extensions are provided as arrays. + +## Composite Services + +Composite services (registered via extension category `components`) are +a pattern supported by the framework. These allow service instances to +be built from multiple components at run-time; support for this pattern +allows additional bundles to introduce or modify behavior associated +with these services without modifying or replacing original service +instances. + +```nomnoml +#direction: down +[ FooService] +[FooDecorator #1]--:>[FooService] +[FooDecorator #n]--:>[FooService] +[FooAggregator]--:>[FooService] +[FooProvider #1]--:>[FooService] +[FooProvider #n]--:>[FooService] + +[FooDecorator #1]o->[ ...decorators...] +[...decorators...]o->[FooDecorator #n] +[FooDecorator #n]o->[FooAggregator] +[FooAggregator]o->[FooProvider #1] +[FooAggregator]o->[ ...providers...] +[FooAggregator]o->[FooProvider #n] + +[FooDecorator #1]--[ Exposed as fooService] +``` + +In this pattern, components all implement an interface which is +standardized for that service. Components additionally declare +that they belong to one of three types: + +* __Providers.__ A provider actually implements the behavior + (satisfies the contract) for that kind of service. For instance, + if a service is responsible for looking up documents by an identifier, + one provider may do so by querying a database, while another may + do so by reading a static JSON document. From the outside, either + provider would look the same (they expose the same interface) and + they could be swapped out easily. +* __Aggregator.__ An aggregator takes many providers and makes them + behave as one. Again, this implements the same interface as an + individual provider, so users of the service do not need to be + concerned about the difference between consulting many providers + and consulting one. Continuing with the example of a service that + looks up documents by identifiers, an aggregator here might consult + all providers, and return any document is found (perhaps picking one + over the other or merging documents if there are multiple matches.) +* __Decorators.__ A decorator exposes the same interface as other + components, but instead of fully implementing the behavior associated + with that kind of service, it only acts as an intermediary, delegating + the actual behavior to a different component. Decorators may transform + inputs or outputs, or initiate some side effects associated with a + service. This is useful if certain common behavior associated with a + service (caching, for instance) may be useful across many different + implementations of that same service. + +The framework will register extensions in this category such that an +aggregator will depend on all of its providers, and decorators will +depend upon on one another in a chain. The result of this compositing step +(the last decorator, if any; otherwise the aggregator, if any; +otherwise a single provider) will be exposed as a single service that +other extensions can acquire through dependency injection. Because all +components of the same type of service expose the same interface, users +of that service do not need to be aware that they are talking to an +aggregator or a provider, for instance. \ No newline at end of file diff --git a/docs/src/architecture/Platform.md b/docs/src/architecture/Platform.md new file mode 100644 index 0000000000..80f9e487f5 --- /dev/null +++ b/docs/src/architecture/Platform.md @@ -0,0 +1,714 @@ +# Overview + +The Open MCT Web platform utilizes the [framework layer](Framework.md) +to provide an extensible baseline for applications which includes: + +* A common user interface (and user interface paradigm) for dealing with + domain objects of various sorts. +* A variety of extension points for introducing new functionality + of various kinds within the context of the common user interface. +* A service infrastructure to support building additional components. + +## Platform Architecture + +While the framework provides a more general architectural paradigm for +building application, the platform adds more specificity by defining +additional extension types and allowing for integration with back end +components. + +The run-time architecture of an Open MCT Web application can be categorized +into certain high-level tiers: + +```nomnoml +[DOM]->[ AngularJS] +[AngularJS]->[Presentation Layer] +[Presentation Layer]->[Information Model] +[Presentation Layer]->[Service Infrastructure] +[Information Model]->[Service Infrastructure] +[Service Infrastructure]->[ Browser APIs] +[Browser APIs]->[Back-end] +``` + +Applications built using Open MCT Web may add or configure functionality +in __any of these tiers__. + +* _DOM_: The rendered HTML document, composed from HTML templates which + have been processed by AngularJS and will be updated by AngularJS + to reflect changes from the presentation layer. User interactions + are initiated from here and invoke behavior in the presentation layer. +* [_Presentation layer_](#presentation-layer): The presentation layer + is responsible for updating (and providing information to update) + the displayed state of the application. The presentation layer consists + primarily of _controllers_ and _directives_. The presentation layer is + concerned with inspecting the information model and preparing it for + display. +* [_Information model_](#information-model): The information model + describes the state and behavior of the objects with which the user + interacts. +* [_Service infrastructure_](#service-infrastructure): The service + infrastructure is responsible for providing the underlying general + functionality needed to support the information model. This includes + exposing underlying sets of extensions and mediating with the + back-end. +* _Back-end_: The back-end is out of the scope of Open MCT Web, except + for the interfaces which are utilized by adapters participating in the + service infrastructure. + +## Application Start-up + +Once the +[application has been initialized](Framework.md#application-initialization) +Open MCT Web primarily operates in an event-driven paradigm; various +events (mouse clicks, timers firing, receiving responses to XHRs) trigger +the invocation of functions, typically in the presentation layer for +user actions or in the service infrastructure for server responses. + +The "main point of entry" into an initialized Open MCT Web application +is effectively the +[route](https://docs.angularjs.org/api/ngRoute/service/$route#example) +which is associated with the URL used to access Open MCT Web (or a +default route.) This route will be associated with a template which +will be displayed; this template will include references to directives +and controllers which will be interpreted by Angular and used to +initialize the state of the display in a manner which is backed by +both the information model and the service infrastructure. + +```nomnoml +[ Start]->[ page load] +[page load]->[ route selection] +[route selection]->[ compile, display template] +[compile, display template]->[Template] +[Template]->[ use Controllers] +[Template]->[ use Directives] +[use Controllers]->[Controllers] +[use Directives]->[Directives] +[Controllers]->[ consult information model] +[consult information model]->[ expose data] +[expose data]->[Angular] +[Angular]->[ update display] +[Directives]->[ add event listeners] +[Directives]->[ update display] +[add event listeners]->[ End] +[update display]->[ End] +``` + + +# Presentation Layer + +The presentation layer of Open MCT Web is responsible for providing +information to display within templates, and for handling interactions +which are initiated from templated DOM elements. AngularJS acts as +an intermediary between the web page as the user sees it, and the +presentation layer implemented as Open MCT Web extensions. + +```nomnoml +[Presentation Layer| + [Angular built-ins| + [routes] + [controllers] + [directives] + [templates] + ] + [Domain object representation| + [views] + [representations] + [representers] + [gestures] + ] +] +``` + +## Angular built-ins + +Several extension categories in the presentation layer map directly +to primitives from AngularJS: + +* [_Controllers_](https://docs.angularjs.org/guide/controller) provide + data to templates, and expose functionality that can be called from + templates. +* [_Directives_](https://docs.angularjs.org/guide/directive) effectively + extend HTML to provide custom behavior associated with specific + attributes and tags. +* [_Routes_](https://docs.angularjs.org/api/ngRoute/service/$route#example) + are used to associate specific URLs (including the fragment identifier) + with specific application states. (In Open MCT Web, these are used to + describe the mode of usage - e.g. browse or edit - as well as to + identify the object being used.) +* [_Templates_](https://docs.angularjs.org/guide/templates) are partial + HTML documents that will be rendered and kept up-to-date by AngularJS. + Open MCT Web introduces a custom `mct-include` directive which acts + as a wrapper around `ng-include` to allow templates to be referred + to by symbolic names. + +## Domain object representation + +The remaining extension categories in the presentation layer are specific +to displaying domain objects. + +* _Representations_ are templates that will be used to display + domain objects in specific ways (e.g. "as a tree node.") +* _Views_ are representations which are exposed to the user as options + for displaying domain objects. +* _Representers_ are extensions which modify or augment the process + of representing domain objects generally (e.g. by attaching + gestures to them.) +* _Gestures_ provide associations between specific user actions + (expressed as DOM events) and resulting behavior upon domain objects + (typically expressed as members of the `actions` extension category) + that can be reused across domain objects. For instance, `drag` and + `drop` are both gestures associated with using drag-and-drop to + modify the composition of domain objects by interacting with their + representations. + +# Information Model + +```nomnoml +#direction: right +[Information Model| + [DomainObject| + getId() : string + getModel() : object + getCapability(key : string) : Capability + hasCapability(key : string) : boolean + useCapability(key : string, args...) : * + ] + [DomainObject] 1 +- 1 [Model] + [DomainObject] 1 o- * [Capability] +] +``` + +Domain objects are the most fundamental component of Open MCT Web's +information model. A domain object is some distinct thing relevant to a +user's work flow, such as a telemetry channel, display, or similar. +Open MCT Web is a tool for viewing, browsing, manipulating, and otherwise +interacting with a graph of domain objects. + +A domain object should be conceived of as the union of the following: + +* _Identifier_: A machine-readable string that uniquely identifies the + domain object within this application instance. +* _Model_: The persistent state of the domain object. A domain object's + model is a JavaScript object that can be losslessly converted to JSON. +* _Capabilities_: Dynamic behavior associated with the domain object. + Capabilities are JavaScript objects which provide additional methods + for interacting with the domain objects which expose those capabilities. + Not all domain objects expose all capabilities. The interface exposed + by any given capability will depend on its type (as identified + by the `key` argument.) For instance, a `persistence` capability + has a different interface from a `telemetry` capability. Using + capabilities requires some prior knowledge of their interface. + +## Capabilities and Services + +```nomnoml +#direction: right +[DomainObject]o-[FooCapability] +[FooCapability]o-[FooService] +[FooService]o-[foos] +``` + +At run-time, the user is primarily concerned with interacting with +domain objects. These interactions are ultimately supported via back-end +services, but to allow customization per-object, these are often mediated +by capabilities. + +A common pattern that emerges in the Open MCT Platform is as follows: + +* A `DomainObject` has some particular behavior that will be supported + by a service. +* A `Capability` of that domain object will define that behavior, + _for that domain object_, supported by a service. +* A `Service` utilized by that capability will perform the actual behavior. +* An extension category will be utilized by that capability to determine + the set of possible behaviors. + +Concrete examples of capabilities which follow this pattern +(or a subset of this pattern) include: + +```nomnoml +#direction: right +[DomainObject]1 o- *[Capability] +[Capability]<:--[TypeCapability] +[Capability]<:--[ActionCapability] +[Capability]<:--[PersistenceCapability] +[Capability]<:--[TelemetryCapability] +[TypeCapability]o-[TypeService] +[TypeService]o-[types] +[ActionCapability]o-[ActionService] +[ActionService]o-[actions] +[PersistenceCapability]o-[PersistenceService] +[TelemetryCapability]o-[TelemetryService] +``` + +# Service Infrastructure + +Most services exposed by the Open MCT Web platform follow the +[composite services](Framework.md#composite-services) to permit +a higher degree of flexibility in how a service can be modified +or customized for specific applications. + +To simplify usage for plugin developers, the platform also usually +includes a provider implementation for these service type that consumes +some extension category. For instance, an `ActionService` provider is +included which depends upon extension category `actions`, and exposes +all actions declared as such to the system. As such, plugin developers +can simply implement the new actions they wish to be made available without +worrying about the details of composite services or implementing a new +`ActionService` provider; however, the ability to implement a new provider +remains useful when the expressive power of individual extensions is +insufficient. + +```nomnoml +[ Service Infrastructure | + [ObjectService]->[ModelService] + [ModelService]->[PersistenceService] + [ObjectService]->[CapabilityService] + [CapabilityService]->[capabilities] + [capabilities]->[TelemetryService] + [capabilities]->[PersistenceService] + [capabilities]->[TypeService] + [capabilities]->[ActionService] + [capabilities]->[ViewService] + [PersistenceService]->[ Document store] + [TelemetryService]->[ Telemetry source] + [ActionService]->[actions] + [ActionService]->[PolicyService] + [ViewService]->[PolicyService] + [ViewService]->[views] + [PolicyService]->[policies] + [TypeService]->[types] +] +``` + +A short summary of the roles of these services: + +* _[ObjectService](#object-service)_: Allows retrieval of domain objects by + their identifiers; in practice, often the main point of entry into the + [information model](#information-model). +* _[ModelService](#model-service)_: Provides domain object models, retrieved + by their identifier. +* _[CapabilityService](#capability-service)_: Provides capabilities, as they + apply to specific domain objects (as judged from their model.) +* _[TelemetryService](#telemetry-service)_: Provides access to historical + and real-time telemetry data. +* _[PersistenceService](#persistence-service)_: Provides the ability to + store and retrieve documents (such as domain object models.) +* _[ActionService](#action-service)_: Provides distinct user actions that + can take place within the system (typically, upon or using domain objects.) +* _[ViewService](#view-service)_: Provides views for domain objects. A view + is a user-selectable representation of a domain object (in practice, an + HTML template.) +* _[PolicyService](#policy-service)_: Handles decisions about which + behavior are allowed within certain specific contexts. +* _[TypeService](#type-service)_: Provides information to distinguish + different types of domain objects from one another within the system. + +## Object Service + +```nomnoml +#direction: right +[ ObjectService| + getObjects(ids : Array.) : Promise.> +] +[DomainObjectProvider]--:>[ObjectService] +[DomainObjectProvider]o-[ModelService] +[DomainObjectProvider]o-[CapabilityService] +``` + +As domain objects are central to Open MCT Web's information model, +acquiring domain objects is equally important. + +```nomnoml +#direction: right +[ Start]->[ Look up models] +[ Look up models]->[ Look up capabilities] +[ Look up capabilities]->[ Instantiate DomainObject] +[ Instantiate DomainObject]->[ End] +``` + +Open MCT Web includes an implementation of an `ObjectService` which +satisfies this capability by: + +* Consulting the [Model Service](#model-service) to acquire domain object + models by identifier. +* Passing these models to a [Capability Service](#capability-service) to + determine which capabilities are applicable. +* Combining these results together as [DomainObject](#information-model) + instances. + +## Model Service + +```nomnoml +#direction: down +[ ModelService| + getModels(ids : Array.) : Promise.> +] +[StaticModelProvider]--:>[ModelService] +[RootModelProvider]--:>[ModelService] +[PersistedModelProvider]--:>[ModelService] +[ModelAggregator]--:>[ModelService] +[CachingModelDecorator]--:>[ModelService] +[MissingModelDecorator]--:>[ModelService] + +[MissingModelDecorator]o-[CachingModelDecorator] +[CachingModelDecorator]o-[ModelAggregator] +[ModelAggregator]o-[StaticModelProvider] +[ModelAggregator]o-[RootModelProvider] +[ModelAggregator]o-[PersistedModelProvider] + +[PersistedModelProvider]o-[PersistenceService] +[RootModelProvider]o-[roots] +[StaticModelProvider]o-[models] +``` + +The platform's model service is responsible for providing domain object +models (effectively, JSON documents describing the persistent state +associated with domain objects.) These are retrieved by identifier. + +The platform includes multiple components of this variety: + +* `PersistedModelProvider` looks up domain object models from + a persistence store (the [`PersistenceService`](#persistence-service)); + this is how user-created and user-modified + domain object models are retrieved. +* `RootModelProvider` provides domain object models that have been + declared via the `roots` extension category. These will appear at the + top level of the tree hierarchy in the user interface. +* `StaticModelProvider` provides domain object models that have been + declared via the `models` extension category. This is useful for + allowing plugins to expose new domain objects declaratively. +* `ModelAggregator` merges together the results from multiple providers. + If multiple providers return models for the same domain object, + the most recently modified version (as determined by the `modified` + property of the model) is chosen. +* `CachingModelDecorator` caches model instances in memory. This + ensures that only a single instance of a domain object model is + present at any given time within the application, and prevent + redundant retrievals. +* `MissingModelDecorator` adds in placeholders when no providers + have returned domain object models for a specific identifier. This + allows the user to easily see that something was expected to be + present, but wasn't. + +## Capability Service + +```nomnoml +#direction: down +[ CapabilityService| + getCapabilities(model : object) : object. +] +[CoreCapabilityProvider]--:>[CapabilityService] +[QueuingPersistenceCapabilityDecorator]--:>[CapabilityService] + +[CoreCapabilityProvider]o-[capabilities] +[QueuingPersistenceCapabilityDecorator]o-[CoreCapabilityProvider] +``` + +The capability service is responsible for determining which capabilities +are applicable for a given domain object, based on its model. Primarily, +this is handled by the `CoreCapabilityProvider`, which examines +capabilities exposed via the `capabilities` extension category. + +Additionally, `platform/persistence/queue` decorates the persistence +capability specifically to batch persistence attempts among multiple +objects (this allows failures to be recognized and handled in groups.) + +## Telemetry Service + +```nomnoml +[ TelemetryService| + requestData(requests : Array.) : Promise. + subscribe(requests : Array.) : Function +]<--:[TelemetryAggregator] +``` + +The telemetry service is responsible for acquiring telemetry data. + +Notably, the platform does not include any providers for +`TelemetryService`; applications built on Open MCT Web will need to +implement a provider for this service if they wish to expose telemetry +data. This is usually the most important step for integrating Open MCT Web +into an existing telemetry system. + +Requests for telemetry data are usually initiated in the +[presentation layer](#presentation-layer) by some `Controller` referenced +from a view. The `telemetryHandler` service is most commonly used (although +one could also use an object's `telemetry` capability directly) as this +handles capability delegation, by which a domain object such as a Telemetry +Panel can declare that its `telemetry` capability should be handled by the +objects it contains. Ultimately, the request for historical data and the +new subscriptions will reach the `TelemetryService`, and, by way of the +provider(s) which are present for that `TelemetryService`, will pass the +same requests to the back-end. + +```nomnoml +[ Start]->[Controller] +[Controller]->[ declares object of interest] +[declares object of interest]->[TelemetryHandler] +[TelemetryHandler]->[ requests telemetry from capabilities] +[TelemetryHandler]->[ subscribes to telemetry using capabilities] +[requests telemetry from capabilities]->[TelemetryCapability] +[subscribes to telemetry using capabilities]->[TelemetryCapability] +[TelemetryCapability]->[ requests telemetry] +[TelemetryCapability]->[ subscribes to telemetry] +[requests telemetry]->[TelemetryService] +[subscribes to telemetry]->[TelemetryService] +[TelemetryService]->[ issues request] +[TelemetryService]->[ updates subscriptions] +[TelemetryService]->[ listens for real-time data] +[issues request]->[ Telemetry Back-end] +[updates subscriptions]->[Telemetry Back-end] +[listens for real-time data]->[Telemetry Back-end] +[Telemetry Back-end]->[ End] +``` + +The back-end, in turn, is expected to provide whatever historical +telemetry is available to satisfy the request that has been issue. + +```nomnoml +[ Start]->[ Telemetry Back-end] +[Telemetry Back-end]->[ transmits historical telemetry] +[transmits historical telemetry]->[TelemetryService] +[TelemetryService]->[ packages telemetry, fulfills requests] +[packages telemetry, fulfills requests]->[TelemetryCapability] +[TelemetryCapability]->[ unpacks telemetry per-object, fulfills request] +[unpacks telemetry per-object, fulfills request]->[TelemetryHandler] +[TelemetryHandler]->[ exposes data] +[TelemetryHandler]->[ notifies controller] +[exposes data]->[Controller] +[notifies controller]->[Controller] +[Controller]->[ prepares data for template] +[prepares data for template]->[Template] +[Template]->[ displays data] +[displays data]->[ End] +``` + +One peculiarity of this approach is that we package many responses +together at once in the `TelemetryService`, then unpack these in the +`TelemetryCapability`, then repackage these in the `TelemetryHandler`. +The rationale for this is as follows: + +* In the `TelemetryService`, we want to have the ability to combine + multiple requests into one call to the back-end, as many back-ends + will support this. It follows that we give the response as a single + object, packages in a manner that allows responses to individual + requests to be easily identified. +* In the `TelemetryCapability`, we want to provide telemetry for a + _single object_, so the telemetry data gets unpacked. This allows + for the unpacking of data to be handled in a single place, and + also permits a flexible substitution method; domain objects may have + implementations of the `telemetry` capability that do not use the + `TelemetryService` at all, while still maintaining compatibility + with any presentation layer code written to utilize this capability. + (This is true of capabilities generally.) +* In the `TelemetryHandler`, we want to group multiple responses back + together again to make it easy for the presentation layer to consume. + In this case, the grouping is different from what may have occurred + in the `TelemetryService`; this grouping is based on what is expected + to be useful _in a specific view_. The `TelemetryService` + may be receiving requests from multiple views. + +```nomnoml +[ Start]->[ Telemetry Back-end] +[Telemetry Back-end]->[ notifies client of new data] +[notifies client of new data]->[TelemetryService] +[TelemetryService]->[ relevant subscribers?] +[relevant subscribers?] yes ->[ notify subscribers] +[relevant subscribers?] no ->[ ignore] +[ignore]->[ Ignored] +[notify subscribers]->[TelemetryCapability] +[TelemetryCapability]->[ notify listener] +[notify listener]->[TelemetryHandler] +[TelemetryHandler]->[ exposes data] +[TelemetryHandler]->[ notifies controller] +[exposes data]->[Controller] +[notifies controller]->[Controller] +[Controller]->[ prepares data for template] +[prepares data for template]->[Template] +[Template]->[ displays data] +[displays data]->[ End] +``` + +The flow of real-time data is similar, and is handled by a sequence +of callbacks between the presentation layer component which is +interested in data and the telemetry service. Providers in the +telemetry service listen to the back-end for new data (via whatever +mechanism their specific back-end supports), package this data in +the same manner as historical data, and pass that to the callbacks +which are associated with relevant requests. + +## Persistence Service + +```nomnoml +#direction: right +[ PersistenceService| + listSpaces() : Promise.> + listObjects() : Promise.> + createObject(space : string, key : string, document : object) : Promise. + readObject(space : string, key : string, document : object) : Promise. + updateObject(space : string, key : string, document : object) : Promise. + deleteObject(space : string, key : string, document : object) : Promise. +] + +[ElasticPersistenceProvider]--:>[PersistenceService] +[ElasticPersistenceProvider]->[ ElasticSearch] + +[CouchPersistenceProvider]--:>[PersistenceService] +[CouchPersistenceProvider]->[ CouchDB] +``` + +Closely related to the notion of domain objects models is their +persistence. The `PersistenceService` allows these to be saved +and loaded. (Currently, this capability is only used for domain +object models, but the interface has been designed without this idea +in mind; other kinds of documents could be saved and loaded in the +same manner.) + +There is no single definitive implementation of a `PersistenceService` in +the platform. Optional adapters are provided to store and load documents +from CouchDB and ElasticSearch, respectively; plugin authors may also +write additional adapters to utilize different back end technologies. + +## Action Service + +```nomnoml +[ActionService| + getActions(context : ActionContext) : Array. +] +[ActionProvider]--:>[ActionService] +[CreateActionProvider]--:>[ActionService] +[ActionAggregator]--:>[ActionService] +[LoggingActionDecorator]--:>[ActionService] +[PolicyActionDecorator]--:>[ActionService] + +[LoggingActionDecorator]o-[PolicyActionDecorator] +[PolicyActionDecorator]o-[ActionAggregator] +[ActionAggregator]o-[ActionProvider] +[ActionAggregator]o-[CreateActionProvider] + +[ActionProvider]o-[actions] +[CreateActionProvider]o-[TypeService] +[PolicyActionDecorator]o-[PolicyService] +``` + +Actions are discrete tasks or behaviors that can be initiated by a user +upon or using a domain object. Actions may appear as menu items or +buttons in the user interface, or may be triggered by certain gestures. + +Responsibilities of platform components of the action service are as +follows: + +* `ActionProvider` exposes actions registered via extension category + `actions`, supporting simple addition of new actions. Actions are + filtered down to match action contexts based on criteria defined as + part of an action's extension definition. +* `CreateActionProvider` provides the various Create actions which + populate the Create menu. These are driven by the available types, + so do not map easily ot extension category `actions`; instead, these + are generated after looking up which actions are available from the + [`TypeService`](#type-service). +* `ActionAggregator` merges together actions from multiple providers. +* `PolicyActionDecorator` enforces the `action` policy category by + filtering out actions which violate this policy, as determined by + consulting the [`PolicyService`](#policy-service). +* `LoggingActionDecorator` wraps exposed actions and writes to the + console when they are performed. + +## View Service + +```nomnoml +[ViewService| + getViews(domainObject : DomainObject) : Array. +] +[ViewProvider]--:>[ViewService] +[PolicyViewDecorator]--:>[ViewService] + +[ViewProvider]o-[views] +[PolicyViewDecorator]o-[ViewProvider] +``` + +The view service provides views that are relevant to a specified domain +object. A "view" is a user-selectable visualization of a domain object. + +The responsibilities of components of the view service are as follows: + +* `ViewProvider` exposes views registered via extension category + `views`, supporting simple addition of new views. Views are + filtered down to match domain objects based on criteria defined as + part of a view's extension definition. +* `PolicyViewDecorator` enforces the `view` policy category by + filtering out views which violate this policy, as determined by + consulting the [`PolicyService`](#policy-service). + +## Policy Service + +```nomnoml +[PolicyService| + allow(category : string, candidate : object, context : object, callback? : Function) : boolean +] +[PolicyProvider]--:>[PolicyService] +[PolicyProvider]o-[policies] +``` + +The policy service provides a general-purpose extensible decision-making +mechanism; plugins can add new extensions of category `policies` to +modify decisions of a known category. + +Often, the policy service is referenced from a decorator for another +service, to filter down the results of using that service based on some +appropriate policy category. + +The policy provider works by looking up all registered policy extensions +which are relevant to a particular _category_, then consulting each in +order to see if they allow a particular _candidate_ in a particular +_context_; the types for the `candidate` and `context` arguments will +vary depending on the `category`. Any one policy may disallow the +decision as a whole. + + +```nomnoml +[ Start]->[ is something allowed?] +[is something allowed?]->[PolicyService] +[PolicyService]->[ look up relevant policies by category] +[look up relevant policies by category]->[ consult policy #1] +[consult policy #1]->[Policy #1] +[Policy #1]->[ policy #1 allows?] +[policy #1 allows?] no ->[ decision disallowed] +[policy #1 allows?] yes ->[ consult policy #2] +[consult policy #2]->[Policy #2] +[Policy #2]->[ policy #2 allows?] +[policy #2 allows?] no ->[ decision disallowed] +[policy #2 allows?] yes ->[ consult policy #3] +[consult policy #3]->[ ...] +[...]->[ consult policy #n] +[consult policy #n]->[Policy #n] +[Policy #n]->[ policy #n allows?] +[policy #n allows?] no ->[ decision disallowed] +[policy #n allows?] yes ->[ decision allowed] +[decision disallowed]->[ Disallowed] +[decision allowed]->[ Allowed] +``` + +The policy decision is effectively an "and" operation over the individual +policy decisions: That is, all policies must agree to allow a particular +policy decision, and the first policy to disallow a decision will cause +the entire decision to be disallowed. As a consequence of this, policies +should generally be written with a default behavior of "allow", and +should only disallow the specific circumstances they are intended to +disallow. + +## Type Service + +```nomnoml +[TypeService| + listTypes() : Array. + getType(key : string) : Type +] +[TypeProvider]--:>[TypeService] +[TypeProvider]o-[types] +``` + +The type service provides metadata about the different types of domain +objects that exist within an Open MCT Web application. The platform +implementation reads these types in from extension category `types` +and wraps them in a JavaScript interface. \ No newline at end of file diff --git a/docs/src/architecture/index.md b/docs/src/architecture/index.md new file mode 100644 index 0000000000..fd9e0961e1 --- /dev/null +++ b/docs/src/architecture/index.md @@ -0,0 +1,78 @@ +# Introduction + +The purpose of this document is to familiarize developers with the +overall architecture of Open MCT Web. + +The target audience includes: + +* _Platform maintainers_: Individuals involved in developing, + extending, and maintaing capabilities of the platform. +* _Integration developers_: Individuals tasked with integrated + Open MCT Web into a larger system, who need to understand + its inner workings sufficiently to complete this integration. + +As the focus of this document is on architecture, whenever possible +implementation details (such as relevant API or JSON syntax) have been +omitted. These details may be found in the developer guide. + +# Overview + +Open MCT Web is client software: It runs in a web browser and +provides a user interface, while communicating with various +server-side resources through browser APIs. + +```nomnoml +#direction: right +[Client|[Browser|[Open MCT Web]->[Browser APIs]]] +[Server|[Web services]] +[Client]<->[Server] +``` + +While Open MCT Web can be configured to run as a standalone client, +this is rarely very useful. Instead, it is intended to be used as a +display and interaction layer for information obtained from a +variety of back-end services. Doing so requires authoring or utilizing +adapter plugins which allow Open MCT Web to interact with these services. + +Typically, the pattern here is to provide a known interface that +Open MCT Web can utilize, and implement it such that it interacts with +whatever back-end provides the relevant information. +Examples of back-ends that can be utilized in this fashion include +databases for the persistence of user-created objects, or sources of +telemetry data. + +## Software Architecture + +The simplest overview of Open MCT Web is to look at it as a "layered" +architecture, where each layer more clearly specifies the behavior +of the software. + +```nomnoml +#direction: down +[Open MCT Web| + [Platform]<->[Application] + [Framework]->[Application] + [Framework]->[Platform] +] +``` + +These layers are: + +* [_Framework_](Framework.md): The framework layer is responsible for + managing the interactions between application components. It has no + application-specific knowledge; at this layer, we have only + established an abstraction by which different software components + may communicate and/or interact. +* [_Platform_](Platform.md): The platform layer defines the general look, feel, and + behavior of Open MCT Web. This includes user-facing components like + Browse mode and Edit mode, as well as underlying elements of the + information model and the general service infrastructure. +* _Application_: The application layer defines specific features of + an application built on Open MCT Web. This includes adapters to + specific back-ends, new types of things for users to create, and + new ways of visualizing objects within the system. This layer + typically consists of a mix of custom plug-ins to Open MCT Web, + as well as optional features (such as Plot view) included alongside + the platform. + + diff --git a/docs/src/guide/index.md b/docs/src/guide/index.md new file mode 100644 index 0000000000..c575439d48 --- /dev/null +++ b/docs/src/guide/index.md @@ -0,0 +1,3 @@ +# Developer Guide + +This is a placeholder for the developer guide. diff --git a/docs/src/index.html b/docs/src/index.html new file mode 100644 index 0000000000..e84b405234 --- /dev/null +++ b/docs/src/index.html @@ -0,0 +1,36 @@ + + + + + + Open MCT Web Documentation + + + Sections: + + + diff --git a/package.json b/package.json index 1216935f21..d1d675df19 100644 --- a/package.json +++ b/package.json @@ -16,13 +16,21 @@ "karma-jasmine": "^0.1.5", "karma-phantomjs-launcher": "^0.1.4", "karma-requirejs": "^0.2.2", - "requirejs": "^2.1.17" + "requirejs": "^2.1.17", + "marked": "^0.3.5", + "glob": ">= 3.0.0", + "split": "^1.0.0", + "mkdirp": "^0.5.1", + "nomnoml": "^0.0.3", + "canvas": "^1.2.7" }, "scripts": { "start": "node app.js", "test": "karma start --single-run", "jshint": "jshint platform example || exit 0", - "jsdoc": "jsdoc -c jsdoc.json -r -d docs" + "jsdoc": "jsdoc -c jsdoc.json -r -d target/docs/api", + "otherdoc": "node docs/gendocs.js --in docs/src --out target/docs", + "docs": "npm run jsdoc ; npm run otherdoc" }, "repository": { "type": "git", diff --git a/platform/commonUI/general/res/css/theme-espresso.css b/platform/commonUI/general/res/css/theme-espresso.css index 67708bdcee..a12100cc67 100644 --- a/platform/commonUI/general/res/css/theme-espresso.css +++ b/platform/commonUI/general/res/css/theme-espresso.css @@ -119,7 +119,7 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -/* line 5, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 5, ../../../../../../../../.gem/ruby/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, @@ -140,38 +140,38 @@ time, mark, audio, video { font-size: 100%; vertical-align: baseline; } -/* line 22, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 22, ../../../../../../../../.gem/ruby/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ html { line-height: 1; } -/* line 24, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 24, ../../../../../../../../.gem/ruby/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ ol, ul { list-style: none; } -/* line 26, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 26, ../../../../../../../../.gem/ruby/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ table { border-collapse: collapse; border-spacing: 0; } -/* line 28, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 28, ../../../../../../../../.gem/ruby/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ caption, th, td { text-align: left; font-weight: normal; vertical-align: middle; } -/* line 30, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 30, ../../../../../../../../.gem/ruby/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ q, blockquote { quotes: none; } - /* line 103, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ + /* line 103, ../../../../../../../../.gem/ruby/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ q:before, q:after, blockquote:before, blockquote:after { content: ""; content: none; } -/* line 32, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 32, ../../../../../../../../.gem/ruby/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ a img { border: none; } -/* line 116, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 116, ../../../../../../../../.gem/ruby/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, nav, section, summary { display: block; } diff --git a/platform/persistence/elastic/src/ElasticsearchSearchProvider.js b/platform/persistence/elastic/src/ElasticsearchSearchProvider.js new file mode 100644 index 0000000000..af13628af9 --- /dev/null +++ b/platform/persistence/elastic/src/ElasticsearchSearchProvider.js @@ -0,0 +1,213 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define*/ + +/** + * Module defining ElasticsearchSearchProvider. Created by shale on 07/16/2015. + * This is not currently included in the bundle definition. + */ +define( + [], + function () { + "use strict"; + + // JSLint doesn't like underscore-prefixed properties, + // so hide them here. + var ID = "_id", + SCORE = "_score", + DEFAULT_MAX_RESULTS = 100; + + /** + * A search service which searches through domain objects in + * the filetree using ElasticSearch. + * + * @constructor + * @param $http Angular's $http service, for working with urls. + * @param {ObjectService} objectService the service from which + * domain objects can be gotten. + * @param ROOT the constant ELASTIC_ROOT which allows us to + * interact with ElasticSearch. + */ + function ElasticsearchSearchProvider($http, objectService, ROOT) { + + // Add the fuzziness operator to the search term + function addFuzziness(searchTerm, editDistance) { + if (!editDistance) { + editDistance = ''; + } + + return searchTerm.split(' ').map(function (s) { + // Don't add fuzziness for quoted strings + if (s.indexOf('"') !== -1) { + return s; + } else { + return s + '~' + editDistance; + } + }).join(' '); + } + + // Currently specific to elasticsearch + function processSearchTerm(searchTerm) { + var spaceIndex; + + // Cut out any extra spaces + while (searchTerm.substr(0, 1) === ' ') { + searchTerm = searchTerm.substring(1, searchTerm.length); + } + while (searchTerm.substr(searchTerm.length - 1, 1) === ' ') { + searchTerm = searchTerm.substring(0, searchTerm.length - 1); + } + spaceIndex = searchTerm.indexOf(' '); + while (spaceIndex !== -1) { + searchTerm = searchTerm.substring(0, spaceIndex) + + searchTerm.substring(spaceIndex + 1, searchTerm.length); + spaceIndex = searchTerm.indexOf(' '); + } + + // Add fuzziness for completeness + searchTerm = addFuzziness(searchTerm); + + return searchTerm; + } + + // Processes results from the format that elasticsearch returns to + // a list of searchResult objects, then returns a result object + // (See documentation for query for object descriptions) + function processResults(rawResults, timestamp) { + var results = rawResults.data.hits.hits, + resultsLength = results.length, + ids = [], + scores = {}, + searchResults = [], + i; + + // Get the result objects' IDs + for (i = 0; i < resultsLength; i += 1) { + ids.push(results[i][ID]); + } + + // Get the result objects' scores + for (i = 0; i < resultsLength; i += 1) { + scores[ids[i]] = results[i][SCORE]; + } + + // Get the domain objects from their IDs + return objectService.getObjects(ids).then(function (objects) { + var j, + id; + + for (j = 0; j < resultsLength; j += 1) { + id = ids[j]; + + // Include items we can get models for + if (objects[id].getModel) { + // Format the results as searchResult objects + searchResults.push({ + id: id, + object: objects[id], + score: scores[id] + }); + } + } + + return { + hits: searchResults, + total: rawResults.data.hits.total, + timedOut: rawResults.data.timed_out + }; + }); + } + + // For documentation, see query below. + function query(searchTerm, timestamp, maxResults, timeout) { + var esQuery; + + // Check to see if the user provided a maximum + // number of results to display + if (!maxResults) { + // Else, we provide a default value. + maxResults = DEFAULT_MAX_RESULTS; + } + + // If the user input is empty, we want to have no search results. + if (searchTerm !== '' && searchTerm !== undefined) { + // Process the search term + searchTerm = processSearchTerm(searchTerm); + + // Create the query to elasticsearch + esQuery = ROOT + "/_search/?q=" + searchTerm + + "&size=" + maxResults; + if (timeout) { + esQuery += "&timeout=" + timeout; + } + + // Get the data... + return $http({ + method: "GET", + url: esQuery + }).then(function (rawResults) { + // ...then process the data + return processResults(rawResults, timestamp); + }, function (err) { + // In case of error, return nothing. (To prevent + // infinite loading time.) + return {hits: [], total: 0}; + }); + } else { + return {hits: [], total: 0}; + } + } + + return { + /** + * Searches through the filetree for domain objects using a search + * term. This is done through querying elasticsearch. Returns a + * promise for a result object that has the format + * {hits: searchResult[], total: number, timedOut: boolean} + * where a searchResult has the format + * {id: string, object: domainObject, score: number} + * + * Notes: + * * The order of the results is from highest to lowest score, + * as elsaticsearch determines them to be. + * * Uses the fuzziness operator to get more results. + * * More about this search's behavior at + * https://www.elastic.co/guide/en/elasticsearch/reference/current/search-uri-request.html + * + * @param searchTerm The text input that is the query. + * @param timestamp The time at which this function was called. + * This timestamp is used as a unique identifier for this + * query and the corresponding results. + * @param maxResults (optional) The maximum number of results + * that this function should return. + * @param timeout (optional) The time after which the search should + * stop calculations and return partial results. Elasticsearch + * does not guarentee that this timeout will be strictly followed. + */ + query: query + }; + } + + + return ElasticsearchSearchProvider; + } +); \ No newline at end of file diff --git a/platform/persistence/elastic/test/ElasticsearchSearchProviderSpec.js b/platform/persistence/elastic/test/ElasticsearchSearchProviderSpec.js new file mode 100644 index 0000000000..1202ef77e6 --- /dev/null +++ b/platform/persistence/elastic/test/ElasticsearchSearchProviderSpec.js @@ -0,0 +1,115 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,describe,it,expect,beforeEach,jasmine*/ + +/** + * SearchSpec. Created by shale on 07/31/2015. + */ +define( + ["../src/ElasticsearchSearchProvider"], + function (ElasticsearchSearchProvider) { + "use strict"; + + // JSLint doesn't like underscore-prefixed properties, + // so hide them here. + var ID = "_id", + SCORE = "_score"; + + describe("The ElasticSearch search provider ", function () { + var mockHttp, + mockHttpPromise, + mockObjectPromise, + mockObjectService, + mockDomainObject, + provider, + mockProviderResults; + + beforeEach(function () { + mockHttp = jasmine.createSpy("$http"); + mockHttpPromise = jasmine.createSpyObj( + "promise", + [ "then" ] + ); + mockHttp.andReturn(mockHttpPromise); + // allow chaining of promise.then().catch(); + mockHttpPromise.then.andReturn(mockHttpPromise); + + mockObjectService = jasmine.createSpyObj( + "objectService", + [ "getObjects" ] + ); + mockObjectPromise = jasmine.createSpyObj( + "promise", + [ "then" ] + ); + mockObjectService.getObjects.andReturn(mockObjectPromise); + + mockDomainObject = jasmine.createSpyObj( + "domainObject", + [ "getId", "getModel" ] + ); + + provider = new ElasticsearchSearchProvider(mockHttp, mockObjectService, ""); + provider.query(' test "query" ', 0, undefined, 1000); + }); + + it("sends a query to ElasticSearch", function () { + expect(mockHttp).toHaveBeenCalled(); + }); + + it("gets data from ElasticSearch", function () { + var data = { + hits: { + hits: [ + {}, + {} + ], + total: 0 + }, + timed_out: false + }; + data.hits.hits[0][ID] = 1; + data.hits.hits[0][SCORE] = 1; + data.hits.hits[1][ID] = 2; + data.hits.hits[1][SCORE] = 2; + + mockProviderResults = mockHttpPromise.then.mostRecentCall.args[0]({data: data}); + + expect( + mockObjectPromise.then.mostRecentCall.args[0]({ + 1: mockDomainObject, + 2: mockDomainObject + }).hits.length + ).toEqual(2); + }); + + it("returns nothing for an empty string query", function () { + expect(provider.query("").hits).toEqual([]); + }); + + it("returns something when there is an ElasticSearch error", function () { + mockProviderResults = mockHttpPromise.then.mostRecentCall.args[1](); + expect(mockProviderResults).toBeDefined(); + }); + }); + } +); \ No newline at end of file diff --git a/platform/persistence/elastic/test/suite.json b/platform/persistence/elastic/test/suite.json index cc8dc2ce0c..939244c089 100644 --- a/platform/persistence/elastic/test/suite.json +++ b/platform/persistence/elastic/test/suite.json @@ -1,4 +1,5 @@ [ "ElasticIndicator", - "ElasticPersistenceProvider" + "ElasticPersistenceProvider", + "ElasticsearchSearchProvider" ] diff --git a/platform/search/bundle.json b/platform/search/bundle.json new file mode 100644 index 0000000000..6668022939 --- /dev/null +++ b/platform/search/bundle.json @@ -0,0 +1,33 @@ +{ + "name": "Search", + "description": "Allows the user to search through the file tree.", + "extensions": { + "constants": [ + { + "key": "GENERIC_SEARCH_ROOTS", + "value": [ "ROOT" ], + "priority": "fallback" + } + ], + "components": [ + { + "provides": "searchService", + "type": "provider", + "implementation": "GenericSearchProvider.js", + "depends": [ "$q", "$timeout", "objectService", "workerService", "GENERIC_SEARCH_ROOTS" ] + }, + { + "provides": "searchService", + "type": "aggregator", + "implementation": "SearchAggregator.js", + "depends": [ "$q" ] + } + ], + "workers": [ + { + "key": "genericSearchWorker", + "scriptUrl": "GenericSearchWorker.js" + } + ] + } +} diff --git a/platform/search/src/GenericSearchProvider.js b/platform/search/src/GenericSearchProvider.js new file mode 100644 index 0000000000..dae2cab9a9 --- /dev/null +++ b/platform/search/src/GenericSearchProvider.js @@ -0,0 +1,268 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define*/ + +/** + * Module defining GenericSearchProvider. Created by shale on 07/16/2015. + */ +define( + [], + function () { + "use strict"; + + var DEFAULT_MAX_RESULTS = 100, + DEFAULT_TIMEOUT = 1000, + stopTime; + + /** + * A search service which searches through domain objects in + * the filetree without using external search implementations. + * + * @constructor + * @param $q Angular's $q, for promise consolidation. + * @param $timeout Angular's $timeout, for delayed function execution. + * @param {ObjectService} objectService The service from which + * domain objects can be gotten. + * @param {WorkerService} workerService The service which allows + * more easy creation of web workers. + * @param {GENERIC_SEARCH_ROOTS} ROOTS An array of the root + * domain objects' IDs. + */ + function GenericSearchProvider($q, $timeout, objectService, workerService, ROOTS) { + var worker = workerService.run('genericSearchWorker'), + indexed = {}, + pendingQueries = {}; + // pendingQueries is a dictionary with the key value pairs st + // the key is the timestamp and the value is the promise + + // Tell the web worker to add a domain object's model to its list of items. + function indexItem(domainObject) { + var message; + + // undefined check + if (domainObject && domainObject.getModel) { + // Using model instead of whole domain object because + // it's a JSON object. + message = { + request: 'index', + model: domainObject.getModel(), + id: domainObject.getId() + }; + worker.postMessage(message); + } + } + + // Tell the worker to search for items it has that match this searchInput. + // Takes the searchInput, as well as a max number of results (will return + // less than that if there are fewer matches). + function workerSearch(searchInput, maxResults, timestamp, timeout) { + var message = { + request: 'search', + input: searchInput, + maxNumber: maxResults, + timestamp: timestamp, + timeout: timeout + }; + worker.postMessage(message); + } + + // Handles responses from the web worker. Namely, the results of + // a search request. + function handleResponse(event) { + var ids = [], + id; + + // If we have the results from a search + if (event.data.request === 'search') { + // Convert the ids given from the web worker into domain objects + for (id in event.data.results) { + ids.push(id); + } + objectService.getObjects(ids).then(function (objects) { + var searchResults = [], + id; + + // Create searchResult objects + for (id in objects) { + searchResults.push({ + object: objects[id], + id: id, + score: event.data.results[id] + }); + } + + // Resove the promise corresponding to this + pendingQueries[event.data.timestamp].resolve({ + hits: searchResults, + total: event.data.total, + timedOut: event.data.timedOut + }); + }); + } + } + + worker.onmessage = handleResponse; + + // Helper function for getItems(). Indexes the tree. + function indexItems(nodes) { + nodes.forEach(function (node) { + var id = node && node.getId && node.getId(); + + // If we have already indexed this item, stop here + if (indexed[id]) { + return; + } + + // Index each item with the web worker + indexItem(node); + indexed[id] = true; + + + // If this node has children, index those + if (node && node.hasCapability && node.hasCapability('composition')) { + // Make sure that this is async, so doesn't block up page + $timeout(function () { + // Get the children... + node.useCapability('composition').then(function (children) { + $timeout(function () { + // ... then index the children + if (children.constructor === Array) { + indexItems(children); + } else { + indexItems([children]); + } + }, 0); + }); + }, 0); + } + + // Watch for changes to this item, in case it gets new children + if (node && node.hasCapability && node.hasCapability('mutation')) { + node.getCapability('mutation').listen(function (listener) { + if (listener && listener.composition) { + // If the node was mutated to have children, get the child domain objects + objectService.getObjects(listener.composition).then(function (objectsById) { + var objects = [], + id; + + // Get each of the domain objects in objectsById + for (id in objectsById) { + objects.push(objectsById[id]); + } + + indexItems(objects); + }); + } + }); + } + }); + } + + // Converts the filetree into a list + function getItems() { + // Aquire root objects + objectService.getObjects(ROOTS).then(function (objectsById) { + var objects = [], + id; + + // Get each of the domain objects in objectsById + for (id in objectsById) { + objects.push(objectsById[id]); + } + + // Index all of the roots' descendents + indexItems(objects); + }); + } + + // For documentation, see query below + function query(input, timestamp, maxResults, timeout) { + var terms = [], + searchResults = [], + defer = $q.defer(); + + // If the input is nonempty, do a search + if (input !== '' && input !== undefined) { + + // Allow us to access this promise later to resolve it later + pendingQueries[timestamp] = defer; + + // Check to see if the user provided a maximum + // number of results to display + if (!maxResults) { + // Else, we provide a default value + maxResults = DEFAULT_MAX_RESULTS; + } + // Similarly, check if timeout was provided + if (!timeout) { + timeout = DEFAULT_TIMEOUT; + } + + // Send the query to the worker + workerSearch(input, maxResults, timestamp, timeout); + + return defer.promise; + } else { + // Otherwise return an empty result + return {hits: [], total: 0}; + } + } + + // Index the tree's contents once at the beginning + getItems(); + + return { + /** + * Searches through the filetree for domain objects which match + * the search term. This function is to be used as a fallback + * in the case where other search services are not avaliable. + * Returns a promise for a result object that has the format + * {hits: searchResult[], total: number, timedOut: boolean} + * where a searchResult has the format + * {id: string, object: domainObject, score: number} + * + * Notes: + * * The order of the results is not guarenteed. + * * A domain object qualifies as a match for a search input if + * the object's name property contains any of the search terms + * (which are generated by splitting the input at spaces). + * * Scores are higher for matches that have more of the terms + * as substrings. + * + * @param input The text input that is the query. + * @param timestamp The time at which this function was called. + * This timestamp is used as a unique identifier for this + * query and the corresponding results. + * @param maxResults (optional) The maximum number of results + * that this function should return. + * @param timeout (optional) The time after which the search should + * stop calculations and return partial results. + */ + query: query + + }; + } + + + return GenericSearchProvider; + } +); \ No newline at end of file diff --git a/platform/search/src/GenericSearchWorker.js b/platform/search/src/GenericSearchWorker.js new file mode 100644 index 0000000000..69e4104602 --- /dev/null +++ b/platform/search/src/GenericSearchWorker.js @@ -0,0 +1,185 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global self*/ + +/** + * Module defining GenericSearchWorker. Created by shale on 07/21/2015. + */ +(function () { + "use strict"; + + // An array of objects composed of domain object IDs and models + // {id: domainObject's ID, model: domainObject's model} + var indexedItems = []; + + // Helper function for index() + // Checks whether an item with this ID is already indexed + function conainsItem(id) { + var i; + for (i = 0; i < indexedItems.length; i += 1) { + if (indexedItems[i].id === id) { + return true; + } + } + return false; + } + + /** + * Indexes an item to indexedItems. + * + * @param data An object which contains: + * * model: The model of the domain object + * * id: The ID of the domain object + */ + function index(data) { + var message; + + if (!conainsItem(data.id)) { + indexedItems.push({ + id: data.id, + model: data.model + }); + } + } + + // Helper function for serach() + function convertToTerms(input) { + var terms = input; + // Shave any spaces off of the ends of the input + while (terms.substr(0, 1) === ' ') { + terms = terms.substring(1, terms.length); + } + while (terms.substr(terms.length - 1, 1) === ' ') { + terms = terms.substring(0, terms.length - 1); + } + + // Then split it at spaces and asterisks + terms = terms.split(/ |\*/); + + // Remove any empty strings from the terms + while (terms.indexOf('') !== -1) { + terms.splice(terms.indexOf(''), 1); + } + + return terms; + } + + // Helper function for search() + function scoreItem(item, input, terms) { + var name = item.model.name.toLocaleLowerCase(), + weight = 0.65, + score = 0.0, + i; + + // Make the score really big if the item name and + // the original search input are the same + if (name === input) { + score = 42; + } + + for (i = 0; i < terms.length; i += 1) { + // Increase the score if the term is in the item name + if (name.indexOf(terms[i]) !== -1) { + score += 1; + + // Add extra to the score if the search term exists + // as its own term within the items + if (name.split(' ').indexOf(terms[i]) !== -1) { + score += 0.5; + } + } + } + + return score * weight; + } + + /** + * Gets search results from the indexedItems based on provided search + * input. Returns matching results from indexedItems, as well as the + * timestamp that was passed to it. + * + * @param data An object which contains: + * * input: The original string which we are searching with + * * maxNumber: The maximum number of search results desired + * * timestamp: The time identifier from when the query was made + */ + function search(data) { + // This results dictionary will have domain object ID keys which + // point to the value the domain object's score. + var results = {}, + input = data.input.toLocaleLowerCase(), + terms = convertToTerms(input), + message = { + request: 'search', + results: {}, + total: 0, + timestamp: data.timestamp, + timedOut: false + }, + score, + i, + id; + + // If the user input is empty, we want to have no search results. + if (input !== '') { + for (i = 0; i < indexedItems.length; i += 1) { + // If this is taking too long, then stop + if (Date.now() > data.timestamp + data.timeout) { + message.timedOut = true; + break; + } + + // Score and add items + score = scoreItem(indexedItems[i], input, terms); + if (score > 0) { + results[indexedItems[i].id] = score; + message.total += 1; + } + } + } + + // Truncate results if there are more than maxResults + if (message.total > data.maxResults) { + i = 0; + for (id in results) { + message.results[id] = results[id]; + i += 1; + if (i >= data.maxResults) { + break; + } + } + // TODO: This seems inefficient. + } else { + message.results = results; + } + + return message; + } + + self.onmessage = function (event) { + if (event.data.request === 'index') { + index(event.data); + } else if (event.data.request === 'search') { + self.postMessage(search(event.data)); + } + }; +}()); \ No newline at end of file diff --git a/platform/search/src/SearchAggregator.js b/platform/search/src/SearchAggregator.js new file mode 100644 index 0000000000..da267214bf --- /dev/null +++ b/platform/search/src/SearchAggregator.js @@ -0,0 +1,146 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define*/ + +/** + * Module defining SearchAggregator. Created by shale on 07/16/2015. + */ +define( + [], + function () { + "use strict"; + + var DEFUALT_TIMEOUT = 1000, + DEFAULT_MAX_RESULTS = 100; + + /** + * Allows multiple services which provide search functionality + * to be treated as one. + * + * @constructor + * @param $q Angular's $q, for promise consolidation. + * @param {SearchProvider[]} providers The search providers to be + * aggregated. + */ + function SearchAggregator($q, providers) { + + // Remove duplicate objects that have the same ID. Modifies the passed + // array, and returns the number that were removed. + function filterDuplicates(results, total) { + var ids = {}, + numRemoved = 0, + i; + + for (i = 0; i < results.length; i += 1) { + if (ids[results[i].id]) { + // If this result's ID is already there, remove the object + results.splice(i, 1); + numRemoved += 1; + + // Reduce loop index because we shortened the array + i -= 1; + } else { + // Otherwise add the ID to the list of the ones we have seen + ids[results[i].id] = true; + } + } + + return numRemoved; + } + + // Order the objects from highest to lowest score in the array. + // Modifies the passed array, as well as returns the modified array. + function orderByScore(results) { + results.sort(function (a, b) { + if (a.score > b.score) { + return -1; + } else if (b.score > a.score) { + return 1; + } else { + return 0; + } + }); + return results; + } + + // For documentation, see query below. + function queryAll(inputText, maxResults) { + var i, + timestamp = Date.now(), + resultPromises = []; + + if (!maxResults) { + maxResults = DEFAULT_MAX_RESULTS; + } + + // Send the query to all the providers + for (i = 0; i < providers.length; i += 1) { + resultPromises.push( + providers[i].query(inputText, timestamp, maxResults, DEFUALT_TIMEOUT) + ); + } + + // Get promises for results arrays + return $q.all(resultPromises).then(function (resultObjects) { + var results = [], + totalSum = 0, + i; + + // Merge results + for (i = 0; i < resultObjects.length; i += 1) { + results = results.concat(resultObjects[i].hits); + totalSum += resultObjects[i].total; + } + // Order by score first, so that when removing repeats we keep the higher scored ones + orderByScore(results); + totalSum -= filterDuplicates(results, totalSum); + + return { + hits: results, + total: totalSum, + timedOut: resultObjects.some(function (obj) { + return obj.timedOut; + }) + }; + }); + } + + return { + /** + * Sends a query to each of the providers. Returns a promise for + * a result object that has the format + * {hits: searchResult[], total: number, timedOut: boolean} + * where a searchResult has the format + * {id: string, object: domainObject, score: number} + * + * @param inputText The text input that is the query. + * @param maxResults (optional) The maximum number of results + * that this function should return. If not provided, a + * default of 100 will be used. + */ + query: queryAll + }; + } + + return SearchAggregator; + } +); \ No newline at end of file diff --git a/platform/search/test/GenericSearchProviderSpec.js b/platform/search/test/GenericSearchProviderSpec.js new file mode 100644 index 0000000000..bec02653b8 --- /dev/null +++ b/platform/search/test/GenericSearchProviderSpec.js @@ -0,0 +1,157 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,describe,it,expect,beforeEach,jasmine*/ + +/** + * SearchSpec. Created by shale on 07/31/2015. + */ +define( + ["../src/GenericSearchProvider"], + function (GenericSearchProvider) { + "use strict"; + + describe("The generic search provider ", function () { + var mockQ, + mockTimeout, + mockDeferred, + mockObjectService, + mockObjectPromise, + mockDomainObjects, + mockCapability, + mockCapabilityPromise, + mockWorkerService, + mockWorker, + mockRoots = ['root1', 'root2'], + provider, + mockProviderResults; + + beforeEach(function () { + var i; + + mockQ = jasmine.createSpyObj( + "$q", + [ "defer" ] + ); + mockDeferred = jasmine.createSpyObj( + "deferred", + [ "resolve", "reject"] + ); + mockDeferred.promise = "mock promise"; + mockQ.defer.andReturn(mockDeferred); + + mockTimeout = jasmine.createSpy("$timeout"); + + mockObjectService = jasmine.createSpyObj( + "objectService", + [ "getObjects" ] + ); + mockObjectPromise = jasmine.createSpyObj( + "promise", + [ "then", "catch" ] + ); + mockObjectService.getObjects.andReturn(mockObjectPromise); + + + mockWorkerService = jasmine.createSpyObj( + "workerService", + [ "run" ] + ); + mockWorker = jasmine.createSpyObj( + "worker", + [ "postMessage" ] + ); + mockWorkerService.run.andReturn(mockWorker); + + mockDomainObjects = {}; + for (i = 0; i < 4; i += 1) { + mockDomainObjects[i] = ( + jasmine.createSpyObj( + "domainObject", + [ "getId", "getModel", "hasCapability", "getCapability", "useCapability" ] + ) + ); + mockDomainObjects[i].getId.andReturn(i); + mockDomainObjects[i].getCapability.andReturn(mockCapability); + } + // Give the first object children + mockDomainObjects[0].hasCapability.andReturn(true); + mockCapability = jasmine.createSpyObj( + "capability", + [ "invoke", "listen" ] + ); + mockCapabilityPromise = jasmine.createSpyObj( + "promise", + [ "then", "catch" ] + ); + mockCapability.invoke.andReturn(mockCapabilityPromise); + mockDomainObjects[0].getCapability.andReturn(mockCapability); + + provider = new GenericSearchProvider(mockQ, mockTimeout, mockObjectService, mockWorkerService, mockRoots); + }); + + it("indexes tree on initialization", function () { + expect(mockObjectService.getObjects).toHaveBeenCalled(); + expect(mockObjectPromise.then).toHaveBeenCalled(); + + mockObjectPromise.then.mostRecentCall.args[0](mockDomainObjects); + + //mockCapabilityPromise.then.mostRecentCall.args[0](mockDomainObjects[1]); + + expect(mockWorker.postMessage).toHaveBeenCalled(); + }); + + it("sends search queries to the worker", function () { + var timestamp = Date.now(); + provider.query(' test "query" ', timestamp, 1, 2); + expect(mockWorker.postMessage).toHaveBeenCalledWith({ + request: "search", + input: ' test "query" ', + timestamp: timestamp, + maxNumber: 1, + timeout: 2 + }); + }); + + it("handles responses from the worker", function () { + var timestamp = Date.now(), + event = { + data: { + request: "search", + results: { + 1: 1, + 2: 2 + }, + total: 2, + timedOut: false, + timestamp: timestamp + } + }; + + provider.query(' test "query" ', timestamp); + mockWorker.onmessage(event); + mockObjectPromise.then.mostRecentCall.args[0](mockDomainObjects); + expect(mockDeferred.resolve).toHaveBeenCalled(); + }); + + }); + } +); \ No newline at end of file diff --git a/platform/search/test/GenericSearchWorkerSpec.js b/platform/search/test/GenericSearchWorkerSpec.js new file mode 100644 index 0000000000..2e17858400 --- /dev/null +++ b/platform/search/test/GenericSearchWorkerSpec.js @@ -0,0 +1,132 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,describe,it,expect,runs,waitsFor,beforeEach,jasmine,Worker,require*/ + +/** + * SearchSpec. Created by shale on 07/31/2015. + */ +define( + [], + function () { + "use strict"; + + describe("The generic search worker ", function () { + // If this test fails, make sure this path is correct + var worker = new Worker(require.toUrl('platform/search/src/GenericSearchWorker.js')), + numObjects = 5; + + beforeEach(function () { + var i; + for (i = 0; i < numObjects; i += 1) { + worker.postMessage( + { + request: "index", + id: i, + model: { + name: "object " + i, + id: i, + type: "something" + } + } + ); + } + }); + + it("searches can reach all objects", function () { + var flag = false, + workerOutput, + resultsLength = 0; + + // Search something that should return all objects + runs(function () { + worker.postMessage( + { + request: "search", + input: "object", + maxNumber: 100, + timestamp: Date.now(), + timeout: 1000 + } + ); + }); + + worker.onmessage = function (event) { + var id; + + workerOutput = event.data; + for (id in workerOutput.results) { + resultsLength += 1; + } + flag = true; + }; + + waitsFor(function () { + return flag; + }, "The worker should be searching", 1000); + + runs(function () { + expect(workerOutput).toBeDefined(); + expect(resultsLength).toEqual(numObjects); + }); + }); + + it("searches return only matches", function () { + var flag = false, + workerOutput, + resultsLength = 0; + + // Search something that should return 1 object + runs(function () { + worker.postMessage( + { + request: "search", + input: "2", + maxNumber: 100, + timestamp: Date.now(), + timeout: 1000 + } + ); + }); + + worker.onmessage = function (event) { + var id; + + workerOutput = event.data; + for (id in workerOutput.results) { + resultsLength += 1; + } + flag = true; + }; + + waitsFor(function () { + return flag; + }, "The worker should be searching", 1000); + + runs(function () { + expect(workerOutput).toBeDefined(); + expect(resultsLength).toEqual(1); + expect(workerOutput.results[2]).toBeDefined(); + }); + }); + }); + } +); \ No newline at end of file diff --git a/platform/search/test/SearchAggregatorSpec.js b/platform/search/test/SearchAggregatorSpec.js new file mode 100644 index 0000000000..cf35e2928e --- /dev/null +++ b/platform/search/test/SearchAggregatorSpec.js @@ -0,0 +1,101 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,describe,it,expect,beforeEach,jasmine*/ + +/** + * SearchSpec. Created by shale on 07/31/2015. + */ +define( + ["../src/SearchAggregator"], + function (SearchAggregator) { + "use strict"; + + describe("The search aggregator ", function () { + var mockQ, + mockPromise, + mockProviders = [], + aggregator, + mockProviderResults = [], + mockAggregatorResults, + i; + + beforeEach(function () { + mockQ = jasmine.createSpyObj( + "$q", + [ "all" ] + ); + mockPromise = jasmine.createSpyObj( + "promise", + [ "then" ] + ); + for (i = 0; i < 3; i += 1) { + mockProviders.push( + jasmine.createSpyObj( + "mockProvider" + i, + [ "query" ] + ) + ); + mockProviders[i].query.andReturn(mockPromise); + } + mockQ.all.andReturn(mockPromise); + + aggregator = new SearchAggregator(mockQ, mockProviders); + aggregator.query(); + + for (i = 0; i < mockProviders.length; i += 1) { + mockProviderResults.push({ + hits: [ + { + id: i, + score: 42 - i + }, + { + id: i + 1, + score: 42 - (2 * i) + } + ] + }); + } + mockAggregatorResults = mockPromise.then.mostRecentCall.args[0](mockProviderResults); + }); + + it("sends queries to all providers", function () { + for (i = 0; i < mockProviders.length; i += 1) { + expect(mockProviders[i].query).toHaveBeenCalled(); + } + }); + + it("filters out duplicate objects", function () { + expect(mockAggregatorResults.hits.length).toEqual(mockProviders.length + 1); + expect(mockAggregatorResults.total).not.toBeLessThan(mockAggregatorResults.hits.length); + }); + + it("orders results by score", function () { + for (i = 1; i < mockAggregatorResults.hits.length; i += 1) { + expect(mockAggregatorResults.hits[i].score) + .not.toBeGreaterThan(mockAggregatorResults.hits[i - 1].score); + } + }); + + }); + } +); \ No newline at end of file diff --git a/platform/search/test/suite.json b/platform/search/test/suite.json new file mode 100644 index 0000000000..5097bde635 --- /dev/null +++ b/platform/search/test/suite.json @@ -0,0 +1,5 @@ +[ + "SearchAggregator", + "GenericSearchProvider", + "GenericSearchWorker" +] diff --git a/protractor/README b/protractor/README new file mode 100644 index 0000000000..5734e5702d --- /dev/null +++ b/protractor/README @@ -0,0 +1,69 @@ +E2e Protractor Tests. + +1. Instructions: + + 1. 3 Control Scripts located in bin/. + run.js : node script used to start tests + start.js: node script used to setup test(starts node,localstorage and webdriver) + stop.js : node script, kills the 3 process started in start.js. + clean.js: node script used to remove the node_module directory.(clean up directory). + + 2. Use npm(Node Package Mangager) to Run Scripts. + a. cd protractor; + b. npm install; + c. To Run: + -npm start : will start processes need by protractor + -npm stop : will stop the processes need by protractor + -npm run-script run : will execute Protractor Script + -npm run-script all : will execute "start", "run", and "stop" script + +2. Directory Hierachy: + + -protractor: base directory + -common: contains prototype javascript functions that all other tests use. + -Buttons: common prototype functions related to enter fullscreen + -CreateItem: common prototype functions related to creating an item + -drag: common functions to test drag and drop. + -editItem: common functions used to test edit functionality. + -Launch: common script used to navigate the specified website. + -RightMenu: common functions for right click menu(remove). + -create + -e2e tests that creates the specified object. + -delete + -e2e tests that removes the specified object + -logs + -ctrl.sh redirects console output of MMAP, webdriver and elastic search and pipes them to log files. + -UI + -Contains tests that test the UI(drag drop, fullscreen, info bubble) + -conf.js: + -protractor config file. Explained below + -stressTest: + Tests that are used to test for memory leaks. You can use the new tab option on WARP and then open the + timeline in the new tab during the browser.sleep(). Once the test is do the browser will pause and you + can look a the timeline results in the new tab. + + NOTE: Cannot open chrome dev tools on same tab as the test are run on. Protractor uses the dev tools to + exectute the tests. + + -StressTest will create and delete folders. + -StressTestBubble.js: creates manny bubbles. + (Delay variable in InfoGesture.js was changed to 0) +3. Conf.js + Conf.js is used by protractor to setup and execute the tests. + -allScriptsTimeout: gives more time for protractor to synchronize with the page. + -jasmineNodeOpts: Protractor uses jasmine for the tests and jasmine has a default time out 30 seconds + per "it" test. Changed to maximume allowed time 360000 ms + -seleniumAddress: Protractor uses a Selenium server as a "proxy" between the test scripts and the browser + driver. A stand a lone version comes with protractor and to run use "webdriver-manager" + default address is: http://localhost:4444/wd/hub. + -specs[]: Is an array of files. Each File should have a "describe, it" test to be executed by protractor. + -capabilities: Tells protractor what browser to use and any browser arguments. + +4. bundle.json + bundle.json is used by npm to determine dependencies and location of script files. + -Dependencies: + "protractor": Contains protractor and webdriver package. + "psnode": Window/Unix Command, used for list/kill process.(ps aux) + "shelljs": Window/Unix Common JS Commands. eg rm,ls,exec + "sleep": Window/Unix Commands used to sleep the script + "string": Window/Unix Commands for string manipulation. \ No newline at end of file diff --git a/protractor/UI/Fullscreen.js b/protractor/UI/Fullscreen.js index 3c1c785228..532ad318d8 100644 --- a/protractor/UI/Fullscreen.js +++ b/protractor/UI/Fullscreen.js @@ -22,7 +22,7 @@ //TODO Add filter for duplications/ var fullScreenFile = require("../common/Buttons"); -describe('Test Fullscreen', function() { +describe('Enable Fullscreen', function() { var fullScreenClass = new fullScreenFile(); beforeEach(require('../common/Launch')); diff --git a/protractor/UI/InfoBubble.js b/protractor/UI/InfoBubble.js index 274b7966b0..c88e9018d0 100644 --- a/protractor/UI/InfoBubble.js +++ b/protractor/UI/InfoBubble.js @@ -25,7 +25,7 @@ var itemEdit = require("../common/EditItem"); var rightMenu = require("../common/RightMenu"); var Drag = require("../common/drag"); -describe('Test Info Bubble', function() { +describe('Info Bubble', function() { var fullScreenClass = new fullScreenFile(); var createClass = new createItem(); var editItemClass = new itemEdit(); diff --git a/protractor/UI/NewWindow.js b/protractor/UI/NewWindow.js index 7a714d79a2..1e98bbc8b4 100644 --- a/protractor/UI/NewWindow.js +++ b/protractor/UI/NewWindow.js @@ -24,7 +24,7 @@ var createClassFile = require("../common/CreateItem") var itemEdit = require("../common/EditItem"); var rightMenu = require("../common/RightMenu.js"); -describe('Test New Window', function() { +describe('New Window', function() { var fullScreenClass = new fullScreenFile(); var createClass = new createClassFile(); var editItemClass = new itemEdit(); diff --git a/protractor/UI/RightClick.js b/protractor/UI/RightClick.js index 0ae4dd0708..5f7d389313 100644 --- a/protractor/UI/RightClick.js +++ b/protractor/UI/RightClick.js @@ -21,28 +21,64 @@ *****************************************************************************/ var right_click = require("../common/RightMenu.js"); var Create = require("../common/CreateItem") -describe('Right Click Interations', function() { +var itemEdit = require("../common/EditItem"); + +describe('The Right Menu', function() { var clickClass = new right_click(); var createClass = new Create(); + var editItemClass = new itemEdit(); var ITEM_NAME = "Folder"; var ITEM_TYPE = "folder"; var ITEM_MENU_GLYPH = 'F\nFolder'; + var ITEM_GRID_SELECT = 'P\nF\nFolder\n0 Items'; beforeEach(require('../common/Launch')); - it('should delete the specified object', function(){ - createClass.createButton().click(); - var folder = createClass.selectNewItem(ITEM_TYPE); - expect(folder.getText()).toEqual([ ITEM_MENU_GLYPH ]); - browser.sleep(1000); - folder.click() - browser.sleep(1000); - browser.wait(function () { - return element.all(by.model('ngModel[field]')).isDisplayed(); + it('should Dissapear After Delete', function(){ + browser.wait(function() { + createClass.createButton().click(); + return true; + }).then(function (){ + var folder = createClass.selectNewItem(ITEM_TYPE); + expect(folder.getText()).toEqual([ ITEM_MENU_GLYPH ]); + browser.sleep(1000); + folder.click() + }).then(function() { + browser.wait(function () { + return element.all(by.model('ngModel[field]')).isDisplayed(); + }) + createClass.fillFolderForum(ITEM_NAME, ITEM_TYPE).click(); + browser.sleep(1000); + }).then(function (){ + var item = editItemClass.SelectItem(ITEM_GRID_SELECT); + expect(item.count()).toBe(1); + browser.sleep(1000); + }).then(function () { + var MyItem = ">\nF\nMy Items" + element.all(by.repeater('child in composition')).filter(function (ele){ + return ele.getText().then(function(text) { + return text === MyItem; + }); + }).all(by.css('.ui-symbol.view-control.ng-binding.ng-scope')).click(); + var object = element.all(by.repeater('child in composition')).filter(function (ele){ + return ele.getText().then(function(text) { + return text === ">\nF\nFolder"; + }); + }); + browser.sleep(1000) + browser.actions().mouseMove(object.get(0)).perform(); + browser.actions().click(protractor.Button.RIGHT).perform(); + browser.sleep(1000) + var menu = element.all(by.css('.ng-binding')).filter(function (ele){ + return ele.getText().then(function (text) { + return text == "Z\nRemove"; + }) + }) + menu.click(); + browser.sleep(1000) + + expect(menu.isDisplayed()).toBe(false); }) - createClass.fillFolderForum(ITEM_NAME, ITEM_TYPE).click(); - clickClass.delete(ITEM_NAME); - browser.sleep(1000); }); }); diff --git a/protractor/bin/clean.js b/protractor/bin/clean.js new file mode 100755 index 0000000000..82e776901f --- /dev/null +++ b/protractor/bin/clean.js @@ -0,0 +1,15 @@ +#! /usr/bin/env node +var shell = require("shelljs/global"); + +var startdir = process.cwd(); +var command = "npm unlink"; + +console.log("Cleaning Directory") +exec(command, function(code, output) { + if(code != 0){ + console.log('Exit code:', code); + console.log('Program output:', output); + } +}); +console.log("rm -rf node_modules") +rm('-rf', __dirname + "/../node_modules") diff --git a/protractor/bin/ctrl.sh b/protractor/bin/ctrl.sh new file mode 100755 index 0000000000..faf385d2ad --- /dev/null +++ b/protractor/bin/ctrl.sh @@ -0,0 +1,90 @@ +#! /bin/bash +ARGUMENT=$1; + +if [ $# != 1 ]; then + echo "Expected 1 Aurgument. Received " $# 1>&2; + exit 1 +fi +#Start webdrive and http-server +if [ $ARGUMENT == start ]; then + echo "Creating Log Directory ..." + mkdir logs; + + cd .. + node app.js -p 1984 -x platform/persistence/elastic -i example/persistence > protractor/logs/nodeApp.log 2>&1 & + sleep 3; + if grep -iq "Error" protractor/logs/nodeApp.log; then + if grep -iq "minimist" protractor/logs/nodeApp.log; then + echo " Node Failed Because Minimist is not installed" + echo " Installng Minimist ..." + npm install minimist express > protractor/logs/minimist.log 2>&1 & + wait $! + if [ $? != 0 ]; then + echo " Error: minimist" + echo " Check Log file" + echo + else + echo " Started: Minimist" + echo + node app.js -p 1984 -x platform/persistence/elastic -i example/persistence > protractor/logs/nodeApp.log 2>&1 & + if grep -iq "Error" protractor/logs/nodeApp.log; then + echo " Error: node app failed" + echo " Check Log file" + echo + else + echo " Started: node app.js" + echo + fi + fi + else + echo " Error: node app failed" + echo " Check Log file" + echo + fi + else + echo " Started: node app.js" + echo + fi + echo "Starting webdriver ..." + + cd protractor; + webdriver-manager start > logs/webdriver.log 2>&1 & + sleep 3; + if grep -iq "Exception" logs/webdriver.log; then + echo " Error: webdriver-manager" + echo " Check Log file" + echo + else + echo " Started: webdriver-manager" + fi + echo "Starting Elastic Search..." + + elasticsearch > logs/elasticSearch.log 2>&1 & + sleep 3; + if grep -iq "Exception" logs/elasticSearch.log; then + echo " Error: ElasticSearch" + echo " Check Log file" + echo + else + echo " Started: ElasticSearch" + fi +#Runs Protractor tests +elif [ $ARGUMENT == run ]; then + protractor ./conf.js +#Kill Process +elif [ $ARGUMENT == stop ]; then + echo "Removing logs" + rm -rf logs + echo "Stopping Node" + kill $(ps aux | grep "[n]ode app.js"| awk '{print $2}'); + + echo "Stopping webdriver ..." + kill $(ps aux | grep "[p]rotractor" | awk '{print $2}'); + kill $(ps aux | grep "[w]ebdriver-manager" | awk '{print $2}'); + sleep 1; + echo "Stopping Elastic..." + kill $(ps aux | grep "[e]lastic" | awk '{print $2}'); + sleep 1; +else + echo "Unkown: Command" $1; +fi diff --git a/protractor/bin/run.js b/protractor/bin/run.js new file mode 100755 index 0000000000..316caa11d0 --- /dev/null +++ b/protractor/bin/run.js @@ -0,0 +1,12 @@ +#! /usr/bin/env node +var shell = require("shelljs/global"); +var sleep = require('sleep'); + +var command = __dirname + "/../node_modules/protractor/bin/protractor " +__dirname + "/../conf.js"; +console.log("Executing Protractor Test") +exec(command, function(code, output) { + if(code != 0){ + console.log('Exit code:', code); + console.log('Program output:', output); + } +}); \ No newline at end of file diff --git a/protractor/bin/start.js b/protractor/bin/start.js new file mode 100755 index 0000000000..21aacc7efe --- /dev/null +++ b/protractor/bin/start.js @@ -0,0 +1,40 @@ +#! /usr/bin/env node +var shell,sleep; +try { + shell = require("shelljs/global"); + sleep = require('sleep'); +}catch (e){ + console.log("Dependencies Error"); + console.log("Run npm install"); + throw (e); +} +///Users/jsanderf/git/elastic/wtd/protractor/bin +var startdir = process.cwd(); +var command; +mkdir(__dirname + '/../logs'); + +command = __dirname + "/../node_modules/protractor/bin/webdriver-manager update"; +console.log("Installing Webdriver"); +exec(command,{async:false}); +sleep.sleep(1); + +console.log(); +cd(__dirname + '/../../'); +console.log('Installing Dependencies'); +exec("npm install minimist express", {async:false}); +console.log('Starting Node'); +sleep.sleep(1); +exec("node app.js -p 1984 -x example/persistence -x platform/persistence/elastic -i example/localstorage > protractor/logs/nodeApp.log 2>&1 &", {async:false}); +console.log(' Started Node'); + +console.log(); +console.log('Starting Webdriver'); +sleep.sleep(1); +exec("protractor/node_modules/protractor/bin/webdriver-manager start --standalone> protractor/logs/webdriver.log 2>&1 &",{async:false}); +if(error() == null){ + console.log(" Webdriver Started"); +}else{ + console.log(" Error : ", error()); +} +sleep.sleep(1); +cd(startdir); diff --git a/protractor/bin/stop.js b/protractor/bin/stop.js new file mode 100755 index 0000000000..ac2c3b4295 --- /dev/null +++ b/protractor/bin/stop.js @@ -0,0 +1,44 @@ +#! /usr/bin/env node + +var shell = require("shelljs/global"); +var ps = require('psnode'); +var S = require('string'); +var sleep = require('sleep'); + +// A simple pid lookup +ps.list(function(err, results) { + + results.forEach(function( process ){ + //Killing Node + if(S(process.command).contains("node app.js")) { + console.log(); + console.log( 'Killing Node: %s', process.command); + ps.kill(process.pid, function(err, stdout) { + if (err) { + throw new Error(err); + } + console.log(stdout); + }); + }else if(S(process.command).contains("webdriver")) { + console.log(); + console.log( 'Killing WebDriver: %s', process.command); + ps.kill(process.pid, function(err, stdout) { + if (err){ + throw new Error(err); + } + console.log(stdout); + }); + }else if(S(process.command).contains("protractor")) { + console.log(); + console.log( 'Killing Chrome Drive: %s', process.command); + ps.kill(process.pid, function(err, stdout) { + if (err){ + throw new Error(err); + } + console.log(stdout); + }); + } + }); +}); + + diff --git a/protractor/common/Launch.js b/protractor/common/Launch.js index 2b700672cc..fd745f94c3 100644 --- a/protractor/common/Launch.js +++ b/protractor/common/Launch.js @@ -24,6 +24,6 @@ module.exports = function launch() { 'use strict'; browser.ignoreSynchronization = true; - browser.get('http://localhost:1984/'); - browser.sleep(2000); // 20 seconds + browser.get('http://localhost:1984'); + browser.sleep(2000); // 2 seconds }; diff --git a/protractor/common/RightMenu.js b/protractor/common/RightMenu.js index 490d876d87..d1375d0533 100644 --- a/protractor/common/RightMenu.js +++ b/protractor/common/RightMenu.js @@ -24,18 +24,25 @@ var RightMenu = (function () { function RightMenu() { } + function carrotMyItem(){ + var MyItem = ">\nF\nMy Items" + element.all(by.repeater('child in composition')).filter(function (ele){ + return ele.getText().then(function(text) { + return text === MyItem; + }); + }).all(by.css('.ui-symbol.view-control.ng-binding.ng-scope')).click(); + } //RightMenu Click on Object RightMenu.prototype.delete = function (name, flag) { if(typeof flag === 'undefined'){ flag = true; } if(flag === true){ - var carrot = element.all(by.css('.ui-symbol.view-control.ng-binding.ng-scope')).get(0).click(); + carrotMyItem(); } browser.sleep(1000) var object = element.all(by.repeater('child in composition')).filter(function (ele){ return ele.getText().then(function(text) { - //expect(text).toEqual("3"); return text === name; }); }); @@ -43,7 +50,7 @@ var RightMenu = (function () { browser.actions().mouseMove(object.get(0)).perform(); browser.actions().click(protractor.Button.RIGHT).perform(); browser.sleep(1000) - var remove = element.all(by.css('.ng-binding')).filter(function (ele){ + element.all(by.css('.ng-binding')).filter(function (ele){ return ele.getText().then(function (text) { return text == "Z\nRemove"; }) @@ -58,11 +65,10 @@ var RightMenu = (function () { }); }; RightMenu.prototype.reset = function (name) { - var carrot = element.all(by.css('.ui-symbol.view-control.ng-binding.ng-scope')).click(); + carrotMyItem(); browser.sleep(1000) var object = element.all(by.repeater('child in composition')).filter(function (ele){ return ele.getText().then(function(text) { - //expect(text).toEqual("3"); return text === name; }); }).click(); @@ -75,19 +81,19 @@ var RightMenu = (function () { return text == "r\nRestart at 0"; }) }).click(); + browser.sleep(1000) }; + //click '<', true==yes false==no RightMenu.prototype.select = function(name, flag){ if(typeof flag == "undefined"){ flag = true; } - //click '<', true==yes false==no if(flag == true){ - var carrot = element.all(by.css('.ui-symbol.view-control.ng-binding.ng-scope')).click(); + carrotMyItem(); } browser.sleep(1000) return element.all(by.repeater('child in composition')).filter(function (ele){ return ele.getText().then(function(text) { - // expect(text).toEqual("3"); return text === name; }); }); @@ -96,7 +102,6 @@ var RightMenu = (function () { RightMenu.prototype.dragDrop = function(name){ var object = element.all(by.repeater('child in composition')).filter(function (ele){ return ele.getText().then(function(text) { - //expect(text).toEqual("3"); return text === name; }); }); diff --git a/protractor/conf.js b/protractor/conf.js index c33e196157..8b828fcbd7 100644 --- a/protractor/conf.js +++ b/protractor/conf.js @@ -24,34 +24,34 @@ // conf.js exports.config = { allScriptsTimeout: 500000, - defaultTimeoutInterval: 60000, + jasmineNodeOpts: {defaultTimeoutInterval: 360000}, seleniumAddress: 'http://localhost:4444/wd/hub', - //specs: ['StressTest.js'], + //specs: ['StressTestCarrot.js'], specs: [ - //'create/CreateActivity.js', - //'delete/DeleteActivity.js', - //'create/CreateActivityMode.js', - //'delete/DeleteActivityMode.js', - //'create/CreateActivityMode.js', - //'create/CreateClock.js', - //'delete/DeleteClock.js', + // 'create/CreateActivity.js', + // 'delete/DeleteActivity.js', + // 'create/CreateActivityMode.js', + // 'delete/DeleteActivityMode.js', + // 'create/CreateClock.js', + // 'delete/DeleteClock.js', 'create/CreateDisplay.js', - //'delete/DeleteDisplay.js', + 'delete/DeleteDisplay.js', 'create/CreateFolder.js', - //'delete/DeleteFolder.js', - 'create/CreateTelemetry.js', - //'delete/DeleteTelemetry.js', - //'create/CreateTimeline.js', - //'delete/DeleteTimeline.js', - //'create/CreateTimer.js', - //'delete/DeleteTimer.js', + 'delete/DeleteFolder.js', + // 'create/CreateTelemetry.js', + // 'delete/DeleteTelemetry.js', + // 'create/CreateTimeline.js', + // 'delete/DeleteTimeline.js', + // 'create/CreateTimer.js', + // 'delete/DeleteTimer.js', 'create/CreateWebPage.js', - //'delete/DeleteWebPage.js', + 'delete/DeleteWebPage.js', 'UI/Fullscreen.js', 'create/CreateButton.js', //"UI/DragDrop.js", - //"UI/NewWindow.js", - 'UI/InfoBubble.js' + "UI/NewWindow.js" + //'UI/InfoBubble.js', + //'UI/RightClick.js' ], capabilities: { 'browserName': 'chrome', // or 'safari' @@ -61,7 +61,7 @@ exports.config = { // Allow specifying binary location as an environment variable, // for cases where Chrome is not installed in a usual location. -if (process.env.PROTRACTOR_CHROME_BINARY) { +if (process.env.CHROME_BIN) { exports.config.capabilities.chromeOptions.binary = - process.env.PROTRACTOR_CHROME_BINARY; + process.env.CHROME_BIN; } diff --git a/protractor/create/CreateActivityMode.js b/protractor/create/CreateActivityMode.js index e9469749aa..17ed700ae6 100644 --- a/protractor/create/CreateActivityMode.js +++ b/protractor/create/CreateActivityMode.js @@ -22,7 +22,7 @@ var itemCreate = require("../common/CreateItem"); var itemEdit = require("../common/EditItem"); -describe('Create Web Page', function() { +describe('Create Activity Mode', function() { var createClass = new itemCreate(); var editItemClass = new itemEdit(); var ITEM_NAME = "Activity Mode"; diff --git a/protractor/create/CreateClock.js b/protractor/create/CreateClock.js index d1dc781b58..940db62af4 100644 --- a/protractor/create/CreateClock.js +++ b/protractor/create/CreateClock.js @@ -57,7 +57,7 @@ describe('Create Clock', function() { }); it('should check clock', function () { - function getTime() { + function getTime(flag) { function addZero(time){ if(time < 10){ return '0' + time; @@ -66,7 +66,6 @@ describe('Create Clock', function() { } var currentdate = new Date(); - var month = currentdate.getMonth() + 1; month = addZero(month); @@ -77,6 +76,9 @@ describe('Create Clock', function() { hour = addZero(hour); var second = currentdate.getSeconds(); + if(flag == true) { + second = second + 1; + } second = addZero(second); var minute = currentdate.getMinutes(); @@ -85,17 +87,23 @@ describe('Create Clock', function() { return ("UTC " + currentdate.getFullYear() + "/" + (month) + "/" + day + " " + (hour) + ":" + minute + ":" + second + " PM"); } - - var current,clock; - rightClickClass.select(ITEM_MENU_GLYPH, true).click().then(function () { - browser.sleep(1000); - current = browser.executeScript(getTime); - }).then(function () { - clock = element(by.css('.l-time-display.l-digital.l-clock.s-clock.ng-scope')); - clock.getText().then(function (time) { - expect(current).toEqual(time); - }) + this.addMatchers({ + toBeIn: function(expected){ + var posibilities = Array.isArray(this.actual) ? this.actual : [this.actual]; + return posibilities.indexOf(expected) > -1; + } }) - + rightClickClass.select(ITEM_MENU_GLYPH, true).click().then(function () { + browser.sleep(1000); + browser.executeScript(getTime, false).then(function(current){ + browser.executeScript(getTime, true).then(function(current1) { + var clock = element(by.css('.l-time-display.l-digital.l-clock.s-clock.ng-scope')); + clock.getText().then(function (ele) { + expect([current,current1]).toBeIn(ele); + }) + }); + }); + + }) }); }); diff --git a/protractor/create/CreateTimer.js b/protractor/create/CreateTimer.js index d8059c1f59..e83fedfedf 100644 --- a/protractor/create/CreateTimer.js +++ b/protractor/create/CreateTimer.js @@ -63,7 +63,11 @@ describe('Create Timer', function() { browser.sleep(1000) var timer = element(by.css('.value.ng-binding.active')) timer.getText().then(function (time) { - expect(time).toEqual("0D 00:00:01") + var timerChecker = false; + if(time == "0D 00:00:01" || time == "0D 00:00:02"){ + timerChecker = true; + } + expect(timerChecker).toEqual(true) }) }); diff --git a/protractor/package.json b/protractor/package.json new file mode 100644 index 0000000000..b43b9b1cdb --- /dev/null +++ b/protractor/package.json @@ -0,0 +1,20 @@ +{ + "name": "ProtractorLauncher", + "version": "1.0.0", + "scripts" : { + "start" : "bin/start.js", + "protractor" : "bin/run.js", + "stop" : "bin/stop.js", + "all" : "bin/start.js; bin/run.js; bin/stop.js;", + "clean" : "bin/clean.js" + }, + "dependencies": { + "protractor": "^2.1.0", + "psnode": "0.0.1", + "shelljs": "^0.5.2", + "sleep": "^3.0.0", + "string": "^3.3.1" + }, + "description": "E2e Protractor Tests.", + "license": "ISC" +} diff --git a/protractor/StressTest.js b/protractor/stressTest/StressTest.js similarity index 74% rename from protractor/StressTest.js rename to protractor/stressTest/StressTest.js index 5e4a8b1507..108e431868 100644 --- a/protractor/StressTest.js +++ b/protractor/stressTest/StressTest.js @@ -19,10 +19,9 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -//TODO Add filter for duplications/ -var itemCreate = require("./common/CreateItem"); -var itemEdit = require("./common/EditItem"); -var right_click = require("./common/RightMenu.js"); +var itemCreate = require("../common/CreateItem"); +var itemEdit = require("../common/EditItem"); +var right_click = require("../common/RightMenu.js"); describe('Create Folder', function() { var clickClass = new right_click(); @@ -41,31 +40,35 @@ describe('Create Folder', function() { }); it('should Create new Folder', function(){ browser.sleep(5000); - for(var i=0; i < 50; i++){ + for(var i=0; i < 25; i++){ browser.wait(function() { createClass.createButton().click(); return true; }).then(function (){ var folder = createClass.selectNewItem(ITEM_TYPE); expect(folder.getText()).toEqual([ ITEM_MENU_GLYPH ]); - browser.sleep(1000); + browser.sleep(500); folder.click() }).then(function() { browser.wait(function () { return element.all(by.model('ngModel[field]')).isDisplayed(); }) createClass.fillFolderForum(ITEM_NAME, ITEM_TYPE).click(); - browser.sleep(1000); + browser.sleep(500); }).then(function (){ - browser.sleep(1000); - // if(i === 1){ - clickClass.delete(ITEM_SIDE_SELECT, true); - element.all(by.css('.ui-symbol.view-control.ng-binding.ng-scope')).click(); - // }else{ - browser.sleep(1000); - + browser.sleep(500); + clickClass.delete(ITEM_SIDE_SELECT, true); + //element.all(by.css('.ui-symbol.view-control.ng-binding.ng-scope')).click(); + + + var MyItem = ">\nF\nMy Items" + element.all(by.repeater('child in composition')).filter(function (ele){ + return ele.getText().then(function(text) { + //expect(text).toEqual(MyItem); + return text === MyItem; + }); + }).all(by.css('.ui-symbol.view-control.ng-binding.ng-scope')).click(); // clickClass.delete(ITEM_SIDE_SELECT, false); - // } }); } browser.pause(); diff --git a/protractor/stressTest/StressTestBubble.js b/protractor/stressTest/StressTestBubble.js new file mode 100644 index 0000000000..b06b29c1b9 --- /dev/null +++ b/protractor/stressTest/StressTestBubble.js @@ -0,0 +1,59 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/StressTestBubble.jsStressTestBubble.js +var itemCreate = require("../common/CreateItem"); +var itemEdit = require("../common/EditItem"); +var right_click = require("../common/RightMenu.js"); + +describe('Create Folder', function() { + var clickClass = new right_click(); + var createClass = new itemCreate(); + var editItemClass = new itemEdit(); + var ITEM_NAME = "Folder"; + var ITEM_TYPE = "folder"; + var ITEM_MENU_GLYPH = 'F\nFolder'; + var ITEM_GRID_SELECT = 'P\nF\nFolder\n0 Items'; + var ITEM_SIDE_SELECT = ">\nF\nFolder" + + beforeEach(function() { + browser.ignoreSynchronization = true; + browser.get('http://localhost:1984/warp/'); + browser.sleep(2000); // 20 seconds + }); + it('should Create new Folder', function(){ + browser.sleep(10000); + for(var i=0; i < 1000; i++){ + var object = element.all(by.repeater('child in composition')).filter(function (ele){ + return ele.getText().then(function(text) { + return text === ">\nF\nMy Items"; + }); + }); + //browser.sleep(1000) + browser.actions().mouseMove(object.get(0)).perform(); + //browser.actions().click(protractor.Button.RIGHT).perform(); + + element.all(by.css('.items-holder.grid.abs.ng-scope')).click(); + } + browser.pause(); + + }); + +}); diff --git a/protractor/stressTest/StressTestCreateButton.js b/protractor/stressTest/StressTestCreateButton.js new file mode 100644 index 0000000000..25debf3bba --- /dev/null +++ b/protractor/stressTest/StressTestCreateButton.js @@ -0,0 +1,56 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +var itemCreate = require("../common/CreateItem"); +var itemEdit = require("../common/EditItem"); +var right_click = require("../common/RightMenu.js"); + +describe('Create Folder', function() { + var clickClass = new right_click(); + var createClass = new itemCreate(); + var editItemClass = new itemEdit(); + var ITEM_NAME = "Folder"; + var ITEM_TYPE = "folder"; + var ITEM_MENU_GLYPH = 'F\nFolder'; + var ITEM_GRID_SELECT = 'P\nF\nFolder\n0 Items'; + var ITEM_SIDE_SELECT = ">\nF\nFolder" + + beforeEach(function() { + browser.ignoreSynchronization = true; + browser.get('http://localhost:1984/warp/'); + browser.sleep(2000); // 20 seconds + }); + it('should Create new Folder', function(){ + browser.sleep(10000); + for(var i=0; i < 1000; i++){ + createClass.createButton().click(); + + //browser.sleep(1000) + //browser.actions().mouseMove(object.get(0)).perform(); + //browser.actions().click(protractor.Button.RIGHT).perform(); + + element.all(by.css('.items-holder.grid.abs.ng-scope')).click(); + } + browser.pause(); + + }); + +}); diff --git a/protractor/stressTest/StressTestMenu.js b/protractor/stressTest/StressTestMenu.js new file mode 100644 index 0000000000..d6e30bc5b2 --- /dev/null +++ b/protractor/stressTest/StressTestMenu.js @@ -0,0 +1,55 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +var itemCreate = require("../common/CreateItem"); +var itemEdit = require("../common/EditItem"); +var right_click = require("../common/RightMenu.js"); + +describe('Create Folder', function() { + var clickClass = new right_click(); + var createClass = new itemCreate(); + var editItemClass = new itemEdit(); + var ITEM_NAME = "Folder"; + var ITEM_TYPE = "folder"; + var ITEM_MENU_GLYPH = 'F\nFolder'; + var ITEM_GRID_SELECT = 'P\nF\nFolder\n0 Items'; + var ITEM_SIDE_SELECT = ">\nF\nFolder" + + beforeEach(function() { + browser.ignoreSynchronization = true; + browser.get('http://localhost:1984/warp/'); + browser.sleep(2000); // 20 seconds + }); + it('should Create new Folder', function(){ + browser.sleep(10000); + for(var i=0; i < 1000; i++){ + browser.wait(function() { + createClass.createButton().click(); + return true; + }).then(function (){ + element.all(by.css('.items-holder.grid.abs.ng-scope')).click(); + }) + } + browser.pause(); + + }); + +}); diff --git a/protractor/stressTest/StressTestNewPage.js b/protractor/stressTest/StressTestNewPage.js new file mode 100644 index 0000000000..2b0e82fbb1 --- /dev/null +++ b/protractor/stressTest/StressTestNewPage.js @@ -0,0 +1,61 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +var itemCreate = require("../common/CreateItem"); +var itemEdit = require("../common/EditItem"); +var right_click = require("../common/RightMenu.js"); +var fullScreenFile = require("../common/FullScreen"); + +describe('Create Folder', function() { + var clickClass = new right_click(); + var createClass = new itemCreate(); + var editItemClass = new itemEdit(); + var fullScreenClass = new fullScreenFile(); + + var ITEM_NAME = "Folder"; + var ITEM_TYPE = "folder"; + var ITEM_MENU_GLYPH = 'F\nFolder'; + var ITEM_GRID_SELECT = 'P\nF\nFolder\n0 Items'; + var ITEM_SIDE_SELECT = ">\nF\nFolder" + + beforeEach(function() { + browser.ignoreSynchronization = true; + browser.get('http://localhost:1984/warp/'); + browser.sleep(2000); // 20 seconds + }); + it('should Create new Folder', function(){ + browser.sleep(15000); + for(var i=0; i < 1000; i++){ + fullScreenClass.newWidnow().click(); + + browser.getAllWindowHandles().then(function (handles) { + //browser.driver.switchTo().window(handles[1]); + browser.sleep(1000); + browser.driver.close(); + browser.sleep(1000); + // browser.driver.switchTo().window(handles[0]); + }); + } + browser.pause(); + + }); + +}); diff --git a/protractor/stressTest/StressTestRightClick.js b/protractor/stressTest/StressTestRightClick.js new file mode 100644 index 0000000000..f16f876a90 --- /dev/null +++ b/protractor/stressTest/StressTestRightClick.js @@ -0,0 +1,59 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +var itemCreate = require("../common/CreateItem"); +var itemEdit = require("../common/EditItem"); +var right_click = require("../common/RightMenu.js"); + +describe('Create Folder', function() { + var clickClass = new right_click(); + var createClass = new itemCreate(); + var editItemClass = new itemEdit(); + var ITEM_NAME = "Folder"; + var ITEM_TYPE = "folder"; + var ITEM_MENU_GLYPH = 'F\nFolder'; + var ITEM_GRID_SELECT = 'P\nF\nFolder\n0 Items'; + var ITEM_SIDE_SELECT = ">\nF\nFolder" + + beforeEach(function() { + browser.ignoreSynchronization = true; + browser.get('http://localhost:1984/warp/'); + browser.sleep(2000); // 20 seconds + }); + it('should Create new Folder', function(){ + browser.sleep(8000); + for(var i=0; i < 1000; i++){ + var object = element.all(by.repeater('child in composition')).filter(function (ele){ + return ele.getText().then(function(text) { + return text === ">\nF\nMy Items"; + }); + }); + //browser.sleep(1000) + browser.actions().mouseMove(object.get(0)).perform(); + browser.actions().click(protractor.Button.RIGHT).perform(); + + element.all(by.css('.items-holder.grid.abs.ng-scope')).click(); + } + browser.pause(); + + }); + +});