Adds SPFx React Jest Testing sample (#507)

This commit is contained in:
Velin Georgiev 2018-05-12 02:59:42 +01:00 committed by Vesa Juvonen
parent 13954cc7bc
commit e415148bc5
52 changed files with 24233 additions and 0 deletions

View File

@ -0,0 +1,25 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# change these settings to your own preference
indent_style = space
indent_size = 2
# we recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[{package,bower}.json]
indent_style = space
indent_size = 2

32
samples/react-jest-testing/.gitignore vendored Normal file
View File

@ -0,0 +1,32 @@
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Build generated files
dist
lib
solution
temp
*.sppkg
# Coverage directory used by tools like istanbul
coverage
# OSX
.DS_Store
# Visual Studio files
.ntvs_analysis.dat
.vs
bin
obj
# Resx Generated Code
*.resx.ts
# Styles Generated Code
*.scss.ts

View File

@ -0,0 +1,8 @@
{
"@microsoft/generator-sharepoint": {
"version": "1.4.1",
"libraryName": "react-jest-testing",
"libraryId": "9e16e13b-8d8c-4b89-8de4-dd654c5b6740",
"environment": "spo"
}
}

View File

@ -0,0 +1,119 @@
# SharePoint Framework React Jest Testing sample #
## SPFx-Jest-Enzyme-Sinon unit testing starter kit
## Summary
This sample uses the popular [Jest Testing Framework](https://facebook.github.io/jest/) with a SPFx client side solution. It is a SPFx-Jest-Enzyme-Sinon starter kit so you can start writing and debugging unit tests in typescript for your SPFx solution.
The setup includes unit tests examples, code coverage reports in different formats, visual studio code unit test debug configurations for typescript, setting a coverage threshold (gates) for continuous integration and continuous deployment scenarios.
## Visual Studio Code Typescript debugging support for the Jest unit tests
The Visual Studio Code launch.json has all the debug configurations needed to start debugging the unit tests for your SPFx solution.
There is a _Jest All_ configuration that will execute all the tests on demand.
There is also a _Jest Watch_ (watcher) configuration that **will let live execution or debugging only on the affected by a change unit tests** (if the solution is part of hg/git repo) and will provide immediate feedback if a test passes or fails on component code change. This is good option for Test Driven Development scenarios.
![SharePoint Framework Jest Visual Studio Code - debugging unit test](./assets/Jest-Typescript-VSCode-debugging.png)
## Sinonjs is included as mocking framework for the SPFx solution
The solution also includes [Sinonjs](http://sinonjs.org/) that can be used to spawn spies, stubs and mocks.
## Enzyme is included to extend to unit tests support for React Components
Enzyme is a testing utility for React that makes it easier to assert, manipulate, and traverse your React Components' output.
## Basic unit tests scenarios included to demonstrate how Jest, Sinon and Enzyme can be used to test the SPFx React components
I wrote several unit tests to demonstrate how all testing libraries can be used together to test a React component with business logic and external dependencies included. Examples for mocking promises, pnpjs calls, https calls and spying on methods included for a quick start in unit testing your SPFx solution.
## Built-in Jest code coverage
Jest uses [Istanbul](https://github.com/gotwarlost/istanbul) under the hood to produce various code coverage reports including live VS code terminal output. Such reports can be integrated in CI tools like VSTS (Visual Studio Team Services) or Jenkins.
![SharePoint Framework Jest tests code coverage reports](./assets/SPFx-jest-coverage.png)
### Jest coverage threshold for continuous deployment pipeline setups
Jest coverage thresholds are set to yield error and potentially fail a build or pre-build if there isn't 100% coverage on branches, functions, lines and statements together. The thresholds can be changed by altering the solution packages.json file where the Jest configuration is.
```JavaScript
"coverageThreshold": {
"global": {
"branches": 100,
"functions": 100,
"lines": 100,
"statements": 100
}
}
```
### Generates summary report in JUnit xml format so it can be integrated with VSTS and Jenkins
After the execution of the unit tests a summary report will be generated under the `./jest/summary-jest-junit.xml` path. Because it uses junit xml formatting most of the CI tools can show the summary on a dashboard. Having that is useful for reporting. That summary is generated by [jest-junit (npm module)](https://www.npmjs.com/package/jest-junit).
## Unit tests support for SPFx extensions
The sample uses SPFx web part, but the same setup applies for SPFx extensions and they can simply be added to the solution and unit tested the same way.
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/drop-1.4.1-green.svg)
## Applies to
* [SharePoint Framework](http://dev.office.com/sharepoint/docs/spfx/sharepoint-framework-overview)
* [Office 365 developer tenant](http://dev.office.com/sharepoint/docs/spfx/set-up-your-developer-tenant)
## Prerequisites
- Office 365 subscription with SharePoint Online.
- SharePoint Framework [development environment](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment) already set up.
## Solution
Solution|Author(s)
--------|---------
react-jest-testing | Velin Georgiev ( [@VelinGeorgiev](https://twitter.com/velingeorgiev) )
## Version history
Version|Date|Comments
-------|----|--------
0.0.1|May 9, 2018 | Initial commit
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Minimal Path to Awesome
- Clone this repository.
- Open the command line, navigate to the web part folder and execute:
- `npm i`
- `npm test` **(NOT gulp test)**
## Features
This Web Part illustrates the following concepts on top of the SharePoint Framework:
- Using React for building SharePoint Framework client-side web parts.
- Using Jest Testing Framework for SPFx unit tests.
- Unit tests included to demonstrate how spies, mocks can be used.
- The use of Enzyme to speed up React unit test productivity.
- The use of [SharePoint Patterns and Practices Reusable Client-side Libraries (PnP Js)](https://github.com/pnp/pnpjs).
- Generating unit test code coverage reports for continious integreation tools such as VSTS
- Generating unit test summary reports for continious integreation tools such as VSTS
## Usefull links
- [Jest](https://facebook.github.io/jest/)
- [Jest cheatsheet](https://devhints.io/jest)
- [Sinonjs](http://sinonjs.org/)
- [Enzyme](http://airbnb.io/enzyme/)
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-jest-testing" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -0,0 +1,18 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"ice-cream-shop-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/iceCreamShop/IceCreamShopWebPart.js",
"manifest": "./src/webparts/iceCreamShop/IceCreamShopWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"IceCreamShopWebPartStrings": "lib/webparts/iceCreamShop/loc/{locale}.js"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/copy-assets.schema.json",
"deployCdnPath": "temp/deploy"
}

View File

@ -0,0 +1,7 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "react-jest-testing",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,13 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-jest-testing-client-side-solution",
"id": "9e16e13b-8d8c-4b89-8de4-dd654c5b6740",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true
},
"paths": {
"zippedPackage": "solution/react-jest-testing.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://dev.office.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://localhost:5432/workbench",
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

@ -0,0 +1,45 @@
{
"$schema": "https://dev.office.com/json-schemas/core-build/tslint.schema.json",
// Display errors as warnings
"displayAsWarning": true,
// The TSLint task may have been configured with several custom lint rules
// before this config file is read (for example lint rules from the tslint-microsoft-contrib
// project). If true, this flag will deactivate any of these rules.
"removeExistingRules": true,
// When true, the TSLint task is configured with some default TSLint "rules.":
"useDefaultConfigAsBase": false,
// Since removeExistingRules=true and useDefaultConfigAsBase=false, there will be no lint rules
// which are active, other than the list of rules below.
"lintConfig": {
// Opt-in to Lint rules which help to eliminate bugs in JavaScript
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-case": true,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"valid-typeof": true,
"variable-name": false,
"whitespace": false
}
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

View File

@ -0,0 +1,7 @@
'use strict';
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
build.initialize(gulp);

View File

@ -0,0 +1,244 @@
<?xml version="1.0" ?>
<!DOCTYPE coverage SYSTEM "http://cobertura.sourceforge.net/xml/coverage-04.dtd">
<coverage lines-valid="59" lines-covered="57" line-rate="0.9661" branches-valid="12" branches-covered="12" branch-rate="1" timestamp="1525986947960" complexity="0" version="0.1">
<sources>
<source>C:\Projects\sp-dev-fx-webparts\samples\react-jest-testing</source>
</sources>
<packages>
<package name="components" line-rate="1" branch-rate="1">
<classes>
<class name="IceCreamShop.tsx" filename="src\webparts\iceCreamShop\components\IceCreamShop.tsx" line-rate="1" branch-rate="1">
<methods>
<method name="(anonymous_5)" hits="6" signature="()V">
<lines>
<line number="8" hits="6"/>
</lines>
</method>
<method name="IceCreamShop" hits="18" signature="()V">
<lines>
<line number="9" hits="18"/>
</lines>
</method>
<method name="(anonymous_7)" hits="18" signature="()V">
<lines>
<line number="21" hits="18"/>
</lines>
</method>
<method name="(anonymous_8)" hits="18" signature="()V">
<lines>
<line number="23" hits="18"/>
</lines>
</method>
<method name="(anonymous_9)" hits="18" signature="()V">
<lines>
<line number="24" hits="18"/>
</lines>
</method>
<method name="(anonymous_10)" hits="52" signature="()V">
<lines>
<line number="32" hits="52"/>
</lines>
</method>
<method name="(anonymous_11)" hits="132" signature="()V">
<lines>
<line number="42" hits="132"/>
</lines>
</method>
<method name="(anonymous_12)" hits="5" signature="()V">
<lines>
<line number="81" hits="5"/>
</lines>
</method>
<method name="(anonymous_13)" hits="5" signature="()V">
<lines>
<line number="83" hits="5"/>
</lines>
</method>
<method name="(anonymous_14)" hits="3" signature="()V">
<lines>
<line number="91" hits="3"/>
</lines>
</method>
<method name="(anonymous_15)" hits="2" signature="()V">
<lines>
<line number="98" hits="2"/>
</lines>
</method>
<method name="(anonymous_16)" hits="2" signature="()V">
<lines>
<line number="100" hits="2"/>
</lines>
</method>
<method name="(anonymous_17)" hits="2" signature="()V">
<lines>
<line number="107" hits="2"/>
</lines>
</method>
<method name="(anonymous_18)" hits="2" signature="()V">
<lines>
<line number="110" hits="2"/>
</lines>
</method>
<method name="(anonymous_19)" hits="5" signature="()V">
<lines>
<line number="117" hits="5"/>
</lines>
</method>
</methods>
<lines>
<line number="1" hits="6" branch="false"/>
<line number="2" hits="6" branch="false"/>
<line number="8" hits="6" branch="false"/>
<line number="9" hits="18" branch="true" condition-coverage="100% (2/2)"/>
<line number="21" hits="18" branch="false"/>
<line number="23" hits="18" branch="false"/>
<line number="24" hits="18" branch="false"/>
<line number="26" hits="18" branch="false"/>
<line number="27" hits="18" branch="false"/>
<line number="32" hits="52" branch="false"/>
<line number="33" hits="52" branch="false"/>
<line number="44" hits="132" branch="false"/>
<line number="81" hits="6" branch="false"/>
<line number="83" hits="5" branch="false"/>
<line number="84" hits="5" branch="false"/>
<line number="85" hits="5" branch="false"/>
<line number="86" hits="5" branch="false"/>
<line number="87" hits="5" branch="false"/>
<line number="91" hits="6" branch="false"/>
<line number="92" hits="3" branch="true" condition-coverage="100% (2/2)"/>
<line number="93" hits="1" branch="false"/>
<line number="96" hits="2" branch="false"/>
<line number="97" hits="2" branch="false"/>
<line number="98" hits="2" branch="false"/>
<line number="100" hits="2" branch="false"/>
<line number="101" hits="2" branch="false"/>
<line number="102" hits="2" branch="false"/>
<line number="107" hits="6" branch="false"/>
<line number="108" hits="2" branch="false"/>
<line number="110" hits="2" branch="false"/>
<line number="111" hits="2" branch="false"/>
<line number="112" hits="2" branch="false"/>
<line number="113" hits="2" branch="false"/>
<line number="117" hits="6" branch="false"/>
<line number="118" hits="5" branch="true" condition-coverage="100% (2/2)"/>
<line number="120" hits="6" branch="false"/>
</lines>
</class>
</classes>
</package>
<package name="iceCreamProviders" line-rate="0.9129999999999999" branch-rate="1">
<classes>
<class name="IceCreamFakeProvider.ts" filename="src\webparts\iceCreamShop\iceCreamProviders\IceCreamFakeProvider.ts" line-rate="1" branch-rate="1">
<methods>
<method name="(anonymous_0)" hits="21" signature="()V">
<lines>
<line number="4" hits="21"/>
</lines>
</method>
<method name="(anonymous_2)" hits="16" signature="()V">
<lines>
<line number="6" hits="16"/>
</lines>
</method>
<method name="(anonymous_3)" hits="16" signature="()V">
<lines>
<line number="8" hits="16"/>
</lines>
</method>
<method name="(anonymous_4)" hits="1" signature="()V">
<lines>
<line number="21" hits="1"/>
</lines>
</method>
<method name="(anonymous_5)" hits="1" signature="()V">
<lines>
<line number="23" hits="1"/>
</lines>
</method>
</methods>
<lines>
<line number="4" hits="5" branch="false"/>
<line number="6" hits="5" branch="false"/>
<line number="8" hits="16" branch="false"/>
<line number="17" hits="16" branch="false"/>
<line number="21" hits="5" branch="false"/>
<line number="23" hits="1" branch="false"/>
<line number="25" hits="5" branch="false"/>
</lines>
</class>
<class name="IceCreamPnPJsProvider.ts" filename="src\webparts\iceCreamShop\iceCreamProviders\IceCreamPnPJsProvider.ts" line-rate="0.875" branch-rate="1">
<methods>
<method name="(anonymous_12)" hits="2" signature="()V">
<lines>
<line number="5" hits="2"/>
</lines>
</method>
<method name="IceCreamPnPJsProvider" hits="4" signature="()V">
<lines>
<line number="9" hits="4"/>
</lines>
</method>
<method name="(anonymous_14)" hits="1" signature="()V">
<lines>
<line number="13" hits="1"/>
</lines>
</method>
<method name="(anonymous_15)" hits="1" signature="()V">
<lines>
<line number="15" hits="1"/>
</lines>
</method>
<method name="(anonymous_18)" hits="1" signature="()V">
<lines>
<line number="23" hits="1"/>
</lines>
</method>
<method name="(anonymous_19)" hits="0" signature="()V">
<lines>
<line number="31" hits="0"/>
</lines>
</method>
<method name="(anonymous_20)" hits="1" signature="()V">
<lines>
<line number="35" hits="1"/>
</lines>
</method>
<method name="(anonymous_21)" hits="1" signature="()V">
<lines>
<line number="37" hits="1"/>
</lines>
</method>
<method name="(anonymous_22)" hits="1" signature="()V">
<lines>
<line number="42" hits="1"/>
</lines>
</method>
<method name="(anonymous_23)" hits="0" signature="()V">
<lines>
<line number="43" hits="0"/>
</lines>
</method>
</methods>
<lines>
<line number="5" hits="2" branch="false"/>
<line number="10" hits="4" branch="false"/>
<line number="13" hits="2" branch="false"/>
<line number="15" hits="1" branch="false"/>
<line number="23" hits="1" branch="false"/>
<line number="25" hits="1" branch="false"/>
<line number="26" hits="3" branch="false"/>
<line number="27" hits="3" branch="false"/>
<line number="30" hits="1" branch="false"/>
<line number="31" hits="0" branch="false"/>
<line number="35" hits="2" branch="false"/>
<line number="37" hits="1" branch="false"/>
<line number="38" hits="1" branch="false"/>
<line number="42" hits="1" branch="false"/>
<line number="43" hits="0" branch="false"/>
<line number="46" hits="2" branch="false"/>
</lines>
</class>
</classes>
</package>
</packages>
</coverage>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,223 @@
body, html {
margin:0; padding: 0;
height: 100%;
}
body {
font-family: Helvetica Neue, Helvetica, Arial;
font-size: 14px;
color:#333;
}
.small { font-size: 12px; }
*, *:after, *:before {
-webkit-box-sizing:border-box;
-moz-box-sizing:border-box;
box-sizing:border-box;
}
h1 { font-size: 20px; margin: 0;}
h2 { font-size: 14px; }
pre {
font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
margin: 0;
padding: 0;
-moz-tab-size: 2;
-o-tab-size: 2;
tab-size: 2;
}
a { color:#0074D9; text-decoration:none; }
a:hover { text-decoration:underline; }
.strong { font-weight: bold; }
.space-top1 { padding: 10px 0 0 0; }
.pad2y { padding: 20px 0; }
.pad1y { padding: 10px 0; }
.pad2x { padding: 0 20px; }
.pad2 { padding: 20px; }
.pad1 { padding: 10px; }
.space-left2 { padding-left:55px; }
.space-right2 { padding-right:20px; }
.center { text-align:center; }
.clearfix { display:block; }
.clearfix:after {
content:'';
display:block;
height:0;
clear:both;
visibility:hidden;
}
.fl { float: left; }
@media only screen and (max-width:640px) {
.col3 { width:100%; max-width:100%; }
.hide-mobile { display:none!important; }
}
.quiet {
color: #7f7f7f;
color: rgba(0,0,0,0.5);
}
.quiet a { opacity: 0.7; }
.fraction {
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
font-size: 10px;
color: #555;
background: #E8E8E8;
padding: 4px 5px;
border-radius: 3px;
vertical-align: middle;
}
div.path a:link, div.path a:visited { color: #333; }
table.coverage {
border-collapse: collapse;
margin: 10px 0 0 0;
padding: 0;
}
table.coverage td {
margin: 0;
padding: 0;
vertical-align: top;
}
table.coverage td.line-count {
text-align: right;
padding: 0 5px 0 20px;
}
table.coverage td.line-coverage {
text-align: right;
padding-right: 10px;
min-width:20px;
}
table.coverage td span.cline-any {
display: inline-block;
padding: 0 5px;
width: 100%;
}
.missing-if-branch {
display: inline-block;
margin-right: 5px;
border-radius: 3px;
position: relative;
padding: 0 4px;
background: #333;
color: yellow;
}
.skip-if-branch {
display: none;
margin-right: 10px;
position: relative;
padding: 0 4px;
background: #ccc;
color: white;
}
.missing-if-branch .typ, .skip-if-branch .typ {
color: inherit !important;
}
.coverage-summary {
border-collapse: collapse;
width: 100%;
}
.coverage-summary tr { border-bottom: 1px solid #bbb; }
.keyline-all { border: 1px solid #ddd; }
.coverage-summary td, .coverage-summary th { padding: 10px; }
.coverage-summary tbody { border: 1px solid #bbb; }
.coverage-summary td { border-right: 1px solid #bbb; }
.coverage-summary td:last-child { border-right: none; }
.coverage-summary th {
text-align: left;
font-weight: normal;
white-space: nowrap;
}
.coverage-summary th.file { border-right: none !important; }
.coverage-summary th.pct { }
.coverage-summary th.pic,
.coverage-summary th.abs,
.coverage-summary td.pct,
.coverage-summary td.abs { text-align: right; }
.coverage-summary td.file { white-space: nowrap; }
.coverage-summary td.pic { min-width: 120px !important; }
.coverage-summary tfoot td { }
.coverage-summary .sorter {
height: 10px;
width: 7px;
display: inline-block;
margin-left: 0.5em;
background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
}
.coverage-summary .sorted .sorter {
background-position: 0 -20px;
}
.coverage-summary .sorted-desc .sorter {
background-position: 0 -10px;
}
.status-line { height: 10px; }
/* yellow */
.cbranch-no { background: yellow !important; color: #111; }
/* dark red */
.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
.low .chart { border:1px solid #C21F39 }
.highlighted,
.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
background: #C21F39 !important;
}
/* medium red */
.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
/* light red */
.low, .cline-no { background:#FCE1E5 }
/* light green */
.high, .cline-yes { background:rgb(230,245,208) }
/* medium green */
.cstat-yes { background:rgb(161,215,106) }
/* dark green */
.status-line.high, .high .cover-fill { background:rgb(77,146,33) }
.high .chart { border:1px solid rgb(77,146,33) }
.medium .chart { border:1px solid #666; }
.medium .cover-fill { background: #666; }
.cstat-skip { background: #ddd; color: #111; }
.fstat-skip { background: #ddd; color: #111 !important; }
.cbranch-skip { background: #ddd !important; color: #111; }
span.cline-neutral { background: #eaeaea; }
.medium { background: #eaeaea; }
.coverage-summary td.empty {
opacity: .5;
padding-top: 4px;
padding-bottom: 4px;
line-height: 1;
color: #888;
}
.cover-fill, .cover-empty {
display:inline-block;
height: 12px;
}
.chart {
line-height: 0;
}
.cover-empty {
background: white;
}
.cover-full {
border-right: none !important;
}
pre.prettyprint {
border: none !important;
padding: 0 !important;
margin: 0 !important;
}
.com { color: #999 !important; }
.ignore-none { color: #999; font-weight: normal; }
.wrapper {
min-height: 100%;
height: auto !important;
height: 100%;
margin: 0 auto -48px;
}
.footer, .push {
height: 48px;
}

View File

@ -0,0 +1,63 @@
var jumpToCode = (function init () {
// Classes of code we would like to highlight
var missingCoverageClasses = [ '.cbranch-no', '.cstat-no', '.fstat-no' ];
// We don't want to select elements that are direct descendants of another match
var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > `
// Selecter that finds elements on the page to which we can jump
var selector = notSelector + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b`
// The NodeList of matching elements
var missingCoverageElements = document.querySelectorAll(selector);
var currentIndex;
function toggleClass(index) {
missingCoverageElements.item(currentIndex).classList.remove('highlighted');
missingCoverageElements.item(index).classList.add('highlighted');
}
function makeCurrent(index) {
toggleClass(index);
currentIndex = index;
missingCoverageElements.item(index)
.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' });
}
function goToPrevious() {
var nextIndex = 0;
if (typeof currentIndex !== 'number' || currentIndex === 0) {
nextIndex = missingCoverageElements.length - 1;
} else if (missingCoverageElements.length > 1) {
nextIndex = currentIndex - 1;
}
makeCurrent(nextIndex);
}
function goToNext() {
var nextIndex = 0;
if (typeof currentIndex === 'number' && currentIndex < (missingCoverageElements.length - 1)) {
nextIndex = currentIndex + 1;
}
makeCurrent(nextIndex);
}
return function jump(event) {
switch (event.which) {
case 78: // n
case 74: // j
goToNext();
break;
case 66: // b
case 75: // k
case 80: // p
goToPrevious();
break;
}
};
}());
window.addEventListener('keydown', jumpToCode);

View File

@ -0,0 +1,429 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for components/IceCreamShop.tsx</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="..\prettify.css" />
<link rel="stylesheet" href="..\base.css" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(..\sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1>
<a href="..\index.html">All files</a> / <a href="index.html">components</a> IceCreamShop.tsx
</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Statements</span>
<span class='fraction'>40/40</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>12/12</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>15/15</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Lines</span>
<span class='fraction'>36/36</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a>
<a name='L89'></a><a href='#L89'>89</a>
<a name='L90'></a><a href='#L90'>90</a>
<a name='L91'></a><a href='#L91'>91</a>
<a name='L92'></a><a href='#L92'>92</a>
<a name='L93'></a><a href='#L93'>93</a>
<a name='L94'></a><a href='#L94'>94</a>
<a name='L95'></a><a href='#L95'>95</a>
<a name='L96'></a><a href='#L96'>96</a>
<a name='L97'></a><a href='#L97'>97</a>
<a name='L98'></a><a href='#L98'>98</a>
<a name='L99'></a><a href='#L99'>99</a>
<a name='L100'></a><a href='#L100'>100</a>
<a name='L101'></a><a href='#L101'>101</a>
<a name='L102'></a><a href='#L102'>102</a>
<a name='L103'></a><a href='#L103'>103</a>
<a name='L104'></a><a href='#L104'>104</a>
<a name='L105'></a><a href='#L105'>105</a>
<a name='L106'></a><a href='#L106'>106</a>
<a name='L107'></a><a href='#L107'>107</a>
<a name='L108'></a><a href='#L108'>108</a>
<a name='L109'></a><a href='#L109'>109</a>
<a name='L110'></a><a href='#L110'>110</a>
<a name='L111'></a><a href='#L111'>111</a>
<a name='L112'></a><a href='#L112'>112</a>
<a name='L113'></a><a href='#L113'>113</a>
<a name='L114'></a><a href='#L114'>114</a>
<a name='L115'></a><a href='#L115'>115</a>
<a name='L116'></a><a href='#L116'>116</a>
<a name='L117'></a><a href='#L117'>117</a>
<a name='L118'></a><a href='#L118'>118</a>
<a name='L119'></a><a href='#L119'>119</a>
<a name='L120'></a><a href='#L120'>120</a>
<a name='L121'></a><a href='#L121'>121</a></td><td class="line-coverage quiet"><span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">52x</span>
<span class="cline-any cline-yes">52x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">132x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import * as React from 'react';
import styles from './IceCreamShop.module.scss';
import { IIceCreamShopProps } from './IIceCreamShopProps';
import { escape } from '@microsoft/sp-lodash-subset';
import { IIceCreamShopState } from './IIceCreamShopState';
import { IceCream } from '../iceCreamProviders/IceCream';
&nbsp;
export default class IceCreamShop extends React.Component&lt;IIceCreamShopProps, IIceCreamShopState&gt; {
constructor(props: IIceCreamShopProps) {
super(props);
&nbsp;
this.state = {
iceCreamFlavoursList: [],
quantity: 1,
selectedIceCream: null,
totalPrice: 0,
hasBoughtIceCream: false
};
}
&nbsp;
public componentDidMount(): void {
&nbsp;
this.props.iceCreamProvider.getAll().then((result: IceCream[]) =&gt; {
this.setState((state: IIceCreamShopState): IIceCreamShopState =&gt; {
&nbsp;
state.iceCreamFlavoursList = result;
return state;
});
});
}
&nbsp;
public render(): React.ReactElement&lt;IIceCreamShopProps&gt; {
return (
&lt;div className={styles.ic} id="iceCreamShop"&gt;
&lt;div className={styles.container}&gt;
&lt;div className={styles.row}&gt;
&lt;div className={styles.column}&gt;
&lt;h1 className={styles.title}&gt;{this.props.strings.TitleLabel}&lt;/h1&gt;
&lt;div id="iceCreamFlavoursList"&gt;
{
this.state.iceCreamFlavoursList &amp;&amp;
this.state.iceCreamFlavoursList.map((item, index) =&gt; {
&nbsp;
return &lt;div key={index}&gt;
&nbsp;
&lt;div className={styles.subTitle}&gt;{item.Title}&lt;/div&gt;
&nbsp;
&lt;button data-automationid={`item-${index}`} className={styles.button} onClick={this.selectHandler.bind(this, item)}&gt;
{this.props.strings.GetItLabel} {item.Price}
&lt;/button&gt;
&nbsp;
&lt;/div&gt;;
})
}
&lt;/div&gt;
&nbsp;
{this.state.selectedIceCream &amp;&amp;
&nbsp;
&lt;div id="buyForm"&gt;
&lt;div className={styles.row}&gt;
&lt;label className={styles.subTitle}&gt;{this.props.strings.QuantityLabel}: &lt;/label&gt;
&lt;input type="number" value={this.state.quantity} min="1" onChange={this.quantityChangeHandler.bind(this)} /&gt;
&lt;/div&gt;
&nbsp;
&lt;div className={styles.row}&gt;
&lt;button className={styles.button} id="buyButton" onClick={this.buyHandler.bind(this)}&gt;
{this.props.strings.BuyLabel} x{this.state.quantity} {this.state.selectedIceCream.Title} {this.props.strings.ForLabel} {this.state.totalPrice}
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
}
&nbsp;
{this.state.hasBoughtIceCream &amp;&amp; &lt;p&gt;{this.props.strings.SuccessLabel}!&lt;/p&gt;}
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
);
}
&nbsp;
public selectHandler(iceCream: IceCream): void {
&nbsp;
this.setState((state: IIceCreamShopState): IIceCreamShopState =&gt; {
state.selectedIceCream = iceCream;
state.totalPrice = Math.round((state.quantity * state.selectedIceCream.Price) * 100) / 100;
state.hasBoughtIceCream = false;
return state;
});
}
&nbsp;
public buyHandler(): void {
if (this.isValid() === false) {
return;
}
&nbsp;
const uniqueid = this.state.selectedIceCream.UniqueId;
const quantity = this.state.quantity;
this.props.iceCreamProvider.buy(uniqueid, quantity).then(result =&gt; {
&nbsp;
this.setState((state: IIceCreamShopState): IIceCreamShopState =&gt; {
state.hasBoughtIceCream = true;
return state;
});
});
}
&nbsp;
public quantityChangeHandler(event: React.ChangeEvent&lt;any&gt;) {
const inputValue = event.target.value;
&nbsp;
this.setState((state: IIceCreamShopState): IIceCreamShopState =&gt; {
state.quantity = inputValue;
state.totalPrice = Math.round((inputValue * state.selectedIceCream.Price) * 100) / 100;
return state;
});
}
&nbsp;
public isValid(): boolean {
return this.state.selectedIceCream !== null &amp;&amp; this.state.quantity &gt; 0;
}
}
&nbsp;</pre></td></tr>
</table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage
generated by <a href="https://istanbul.js.org/" target="_blank">istanbul</a> at Thu May 10 2018 22:15:47 GMT+0100 (GMT Daylight Time)
</div>
</div>
<script src="..\prettify.js"></script>
<script>
window.onload = function () {
if (typeof prettyPrint === 'function') {
prettyPrint();
}
};
</script>
<script src="..\sorter.js"></script>
<script src="..\block-navigation.js"></script>
</body>
</html>

View File

@ -0,0 +1,97 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for components</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="..\prettify.css" />
<link rel="stylesheet" href="..\base.css" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(..\sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1>
<a href="..\index.html">All files</a> components
</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Statements</span>
<span class='fraction'>40/40</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>12/12</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>15/15</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Lines</span>
<span class='fraction'>36/36</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
</div>
<div class='status-line high'></div>
<div class="pad1">
<table class="coverage-summary">
<thead>
<tr>
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
</tr>
</thead>
<tbody><tr>
<td class="file high" data-value="IceCreamShop.tsx"><a href="IceCreamShop.tsx.html">IceCreamShop.tsx</a></td>
<td data-value="100" class="pic high"><div class="chart"><div class="cover-fill cover-full" style="width: 100%;"></div><div class="cover-empty" style="width:0%;"></div></div></td>
<td data-value="100" class="pct high">100%</td>
<td data-value="40" class="abs high">40/40</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="12" class="abs high">12/12</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="15" class="abs high">15/15</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="36" class="abs high">36/36</td>
</tr>
</tbody>
</table>
</div><div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage
generated by <a href="https://istanbul.js.org/" target="_blank">istanbul</a> at Thu May 10 2018 22:15:47 GMT+0100 (GMT Daylight Time)
</div>
</div>
<script src="..\prettify.js"></script>
<script>
window.onload = function () {
if (typeof prettyPrint === 'function') {
prettyPrint();
}
};
</script>
<script src="..\sorter.js"></script>
<script src="..\block-navigation.js"></script>
</body>
</html>

View File

@ -0,0 +1,141 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for iceCreamProviders/IceCreamFakeProvider.ts</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="..\prettify.css" />
<link rel="stylesheet" href="..\base.css" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(..\sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1>
<a href="..\index.html">All files</a> / <a href="index.html">iceCreamProviders</a> IceCreamFakeProvider.ts
</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Statements</span>
<span class='fraction'>9/9</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>0/0</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>5/5</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Lines</span>
<span class='fraction'>7/7</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span></td><td class="text"><pre class="prettyprint lang-js">import { IceCream } from "./IceCream";
import { IIceCreamProvider } from "./IIceCreamProvider";
&nbsp;
export class IceCreamFakeProvider implements IIceCreamProvider {
&nbsp;
public getAll(): Promise&lt;Array&lt;IceCream&gt;&gt; {
&nbsp;
return new Promise&lt;Array&lt;IceCream&gt;&gt;(resolve =&gt; {
&nbsp;
let list = [
{ UniqueId: "1", Title: "Cherry" },
{ UniqueId: "2", Title: "Chocolate" },
{ UniqueId: "3", Title: "Coffee and Cookie" },
{ UniqueId: "10", Title: "Vanilla" }
] as IceCream[];
resolve(list);
});
}
&nbsp;
public buy(uniqueid: string, quantity: number): Promise&lt;void&gt; {
&nbsp;
return new Promise&lt;void&gt;(resolve =&gt; resolve());
}
}</pre></td></tr>
</table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage
generated by <a href="https://istanbul.js.org/" target="_blank">istanbul</a> at Thu May 10 2018 22:15:47 GMT+0100 (GMT Daylight Time)
</div>
</div>
<script src="..\prettify.js"></script>
<script>
window.onload = function () {
if (typeof prettyPrint === 'function') {
prettyPrint();
}
};
</script>
<script src="..\sorter.js"></script>
<script src="..\block-navigation.js"></script>
</body>
</html>

View File

@ -0,0 +1,204 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for iceCreamProviders/IceCreamPnPJsProvider.ts</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="..\prettify.css" />
<link rel="stylesheet" href="..\base.css" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(..\sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1>
<a href="..\index.html">All files</a> / <a href="index.html">iceCreamProviders</a> IceCreamPnPJsProvider.ts
</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">90% </span>
<span class="quiet">Statements</span>
<span class='fraction'>18/20</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>0/0</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">80% </span>
<span class="quiet">Functions</span>
<span class='fraction'>8/10</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">87.5% </span>
<span class="quiet">Lines</span>
<span class='fraction'>14/16</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span></td><td class="text"><pre class="prettyprint lang-js">import { IceCream } from "./IceCream";
import { IIceCreamProvider } from "./IIceCreamProvider";
import { SPRest, SearchQuery, SearchResult, SearchResults } from "@pnp/sp";
&nbsp;
export class IceCreamPnPJsProvider implements IIceCreamProvider {
&nbsp;
private readonly sp: SPRest;
&nbsp;
constructor(sp: SPRest) {
this.sp = sp;
}
&nbsp;
public getAll(): Promise&lt;IceCream[]&gt; {
&nbsp;
return new Promise&lt;IceCream[]&gt;(async (resolve, reject) =&gt; {
&nbsp;
const query: SearchQuery = {
RowLimit: 10,
SelectProperties: ["UniqueId", "Title", "PriceOWSNMBR"],
Querytext: 'path:https://spfxjest.sharepoint.com/sites/jest/Lists/IceCreamFlavours AND contenttypeid:0x01*'
} as SearchQuery;
&nbsp;
this.sp.search(query).then((searchResults: SearchResults) =&gt; {
&nbsp;
const result = [];
for (const item of searchResults.PrimarySearchResults) {
result.push({ UniqueId: item.UniqueId, Title: item.Title, Price: Math.round(item["PriceOWSNMBR"] * 100) / 100 });
}
&nbsp;
resolve(result);
}).<span class="fstat-no" title="function not covered" >catch(<span class="cstat-no" title="statement not covered" >error </span>=&gt; reject(error));</span>
});
}
&nbsp;
public buy(uniqueid: string, quantity: number): Promise&lt;any&gt; {
&nbsp;
return new Promise&lt;any&gt;((resolve, reject) =&gt; {
this.sp.web.lists.getByTitle('Ice Cream Orders').items.add({
"Title": uniqueid,
"Quantity": quantity
})
.then(result =&gt; resolve())
.catch(<span class="fstat-no" title="function not covered" >error<span class="cstat-no" title="statement not covered" > =&gt; </span>reject(error));</span>
});
}
}</pre></td></tr>
</table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage
generated by <a href="https://istanbul.js.org/" target="_blank">istanbul</a> at Thu May 10 2018 22:15:47 GMT+0100 (GMT Daylight Time)
</div>
</div>
<script src="..\prettify.js"></script>
<script>
window.onload = function () {
if (typeof prettyPrint === 'function') {
prettyPrint();
}
};
</script>
<script src="..\sorter.js"></script>
<script src="..\block-navigation.js"></script>
</body>
</html>

View File

@ -0,0 +1,110 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for iceCreamProviders</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="..\prettify.css" />
<link rel="stylesheet" href="..\base.css" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(..\sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1>
<a href="..\index.html">All files</a> iceCreamProviders
</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">93.1% </span>
<span class="quiet">Statements</span>
<span class='fraction'>27/29</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>0/0</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">86.67% </span>
<span class="quiet">Functions</span>
<span class='fraction'>13/15</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">91.3% </span>
<span class="quiet">Lines</span>
<span class='fraction'>21/23</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
</div>
<div class='status-line high'></div>
<div class="pad1">
<table class="coverage-summary">
<thead>
<tr>
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
</tr>
</thead>
<tbody><tr>
<td class="file high" data-value="IceCreamFakeProvider.ts"><a href="IceCreamFakeProvider.ts.html">IceCreamFakeProvider.ts</a></td>
<td data-value="100" class="pic high"><div class="chart"><div class="cover-fill cover-full" style="width: 100%;"></div><div class="cover-empty" style="width:0%;"></div></div></td>
<td data-value="100" class="pct high">100%</td>
<td data-value="9" class="abs high">9/9</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="0" class="abs high">0/0</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="5" class="abs high">5/5</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="7" class="abs high">7/7</td>
</tr>
<tr>
<td class="file high" data-value="IceCreamPnPJsProvider.ts"><a href="IceCreamPnPJsProvider.ts.html">IceCreamPnPJsProvider.ts</a></td>
<td data-value="90" class="pic high"><div class="chart"><div class="cover-fill" style="width: 90%;"></div><div class="cover-empty" style="width:10%;"></div></div></td>
<td data-value="90" class="pct high">90%</td>
<td data-value="20" class="abs high">18/20</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="0" class="abs high">0/0</td>
<td data-value="80" class="pct high">80%</td>
<td data-value="10" class="abs high">8/10</td>
<td data-value="87.5" class="pct high">87.5%</td>
<td data-value="16" class="abs high">14/16</td>
</tr>
</tbody>
</table>
</div><div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage
generated by <a href="https://istanbul.js.org/" target="_blank">istanbul</a> at Thu May 10 2018 22:15:47 GMT+0100 (GMT Daylight Time)
</div>
</div>
<script src="..\prettify.js"></script>
<script>
window.onload = function () {
if (typeof prettyPrint === 'function') {
prettyPrint();
}
};
</script>
<script src="..\sorter.js"></script>
<script src="..\block-navigation.js"></script>
</body>
</html>

View File

@ -0,0 +1,110 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for All files</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="prettify.css" />
<link rel="stylesheet" href="base.css" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1>
All files
</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">97.1% </span>
<span class="quiet">Statements</span>
<span class='fraction'>67/69</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>12/12</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">93.33% </span>
<span class="quiet">Functions</span>
<span class='fraction'>28/30</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">96.61% </span>
<span class="quiet">Lines</span>
<span class='fraction'>57/59</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
</div>
<div class='status-line high'></div>
<div class="pad1">
<table class="coverage-summary">
<thead>
<tr>
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
</tr>
</thead>
<tbody><tr>
<td class="file high" data-value="components"><a href="components\index.html">components</a></td>
<td data-value="100" class="pic high"><div class="chart"><div class="cover-fill cover-full" style="width: 100%;"></div><div class="cover-empty" style="width:0%;"></div></div></td>
<td data-value="100" class="pct high">100%</td>
<td data-value="40" class="abs high">40/40</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="12" class="abs high">12/12</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="15" class="abs high">15/15</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="36" class="abs high">36/36</td>
</tr>
<tr>
<td class="file high" data-value="iceCreamProviders"><a href="iceCreamProviders\index.html">iceCreamProviders</a></td>
<td data-value="93.1" class="pic high"><div class="chart"><div class="cover-fill" style="width: 93%;"></div><div class="cover-empty" style="width:7%;"></div></div></td>
<td data-value="93.1" class="pct high">93.1%</td>
<td data-value="29" class="abs high">27/29</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="0" class="abs high">0/0</td>
<td data-value="86.67" class="pct high">86.67%</td>
<td data-value="15" class="abs high">13/15</td>
<td data-value="91.3" class="pct high">91.3%</td>
<td data-value="23" class="abs high">21/23</td>
</tr>
</tbody>
</table>
</div><div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage
generated by <a href="https://istanbul.js.org/" target="_blank">istanbul</a> at Thu May 10 2018 22:15:47 GMT+0100 (GMT Daylight Time)
</div>
</div>
<script src="prettify.js"></script>
<script>
window.onload = function () {
if (typeof prettyPrint === 'function') {
prettyPrint();
}
};
</script>
<script src="sorter.js"></script>
<script src="block-navigation.js"></script>
</body>
</html>

View File

@ -0,0 +1 @@
.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 B

View File

@ -0,0 +1,158 @@
var addSorting = (function () {
"use strict";
var cols,
currentSort = {
index: 0,
desc: false
};
// returns the summary table element
function getTable() { return document.querySelector('.coverage-summary'); }
// returns the thead element of the summary table
function getTableHeader() { return getTable().querySelector('thead tr'); }
// returns the tbody element of the summary table
function getTableBody() { return getTable().querySelector('tbody'); }
// returns the th element for nth column
function getNthColumn(n) { return getTableHeader().querySelectorAll('th')[n]; }
// loads all columns
function loadColumns() {
var colNodes = getTableHeader().querySelectorAll('th'),
colNode,
cols = [],
col,
i;
for (i = 0; i < colNodes.length; i += 1) {
colNode = colNodes[i];
col = {
key: colNode.getAttribute('data-col'),
sortable: !colNode.getAttribute('data-nosort'),
type: colNode.getAttribute('data-type') || 'string'
};
cols.push(col);
if (col.sortable) {
col.defaultDescSort = col.type === 'number';
colNode.innerHTML = colNode.innerHTML + '<span class="sorter"></span>';
}
}
return cols;
}
// attaches a data attribute to every tr element with an object
// of data values keyed by column name
function loadRowData(tableRow) {
var tableCols = tableRow.querySelectorAll('td'),
colNode,
col,
data = {},
i,
val;
for (i = 0; i < tableCols.length; i += 1) {
colNode = tableCols[i];
col = cols[i];
val = colNode.getAttribute('data-value');
if (col.type === 'number') {
val = Number(val);
}
data[col.key] = val;
}
return data;
}
// loads all row data
function loadData() {
var rows = getTableBody().querySelectorAll('tr'),
i;
for (i = 0; i < rows.length; i += 1) {
rows[i].data = loadRowData(rows[i]);
}
}
// sorts the table using the data for the ith column
function sortByIndex(index, desc) {
var key = cols[index].key,
sorter = function (a, b) {
a = a.data[key];
b = b.data[key];
return a < b ? -1 : a > b ? 1 : 0;
},
finalSorter = sorter,
tableBody = document.querySelector('.coverage-summary tbody'),
rowNodes = tableBody.querySelectorAll('tr'),
rows = [],
i;
if (desc) {
finalSorter = function (a, b) {
return -1 * sorter(a, b);
};
}
for (i = 0; i < rowNodes.length; i += 1) {
rows.push(rowNodes[i]);
tableBody.removeChild(rowNodes[i]);
}
rows.sort(finalSorter);
for (i = 0; i < rows.length; i += 1) {
tableBody.appendChild(rows[i]);
}
}
// removes sort indicators for current column being sorted
function removeSortIndicators() {
var col = getNthColumn(currentSort.index),
cls = col.className;
cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, '');
col.className = cls;
}
// adds sort indicators for current column being sorted
function addSortIndicators() {
getNthColumn(currentSort.index).className += currentSort.desc ? ' sorted-desc' : ' sorted';
}
// adds event listeners for all sorter widgets
function enableUI() {
var i,
el,
ithSorter = function ithSorter(i) {
var col = cols[i];
return function () {
var desc = col.defaultDescSort;
if (currentSort.index === i) {
desc = !currentSort.desc;
}
sortByIndex(i, desc);
removeSortIndicators();
currentSort.index = i;
currentSort.desc = desc;
addSortIndicators();
};
};
for (i =0 ; i < cols.length; i += 1) {
if (cols[i].sortable) {
// add the click event handler on the th so users
// dont have to click on those tiny arrows
el = getNthColumn(i).querySelector('.sorter').parentElement;
if (el.addEventListener) {
el.addEventListener('click', ithSorter(i));
} else {
el.attachEvent('onclick', ithSorter(i));
}
}
}
}
// adds sorting functionality to the UI
return function () {
if (!getTable()) {
return;
}
cols = loadColumns();
loadData(cols);
addSortIndicators();
enableUI();
};
})();
window.addEventListener('load', addSorting);

View File

@ -0,0 +1,158 @@
TN:
SF:C:\Projects\sp-dev-fx-webparts\samples\react-jest-testing\src\webparts\iceCreamShop\components\IceCreamShop.tsx
FN:8,(anonymous_5)
FN:9,IceCreamShop
FN:21,(anonymous_7)
FN:23,(anonymous_8)
FN:24,(anonymous_9)
FN:32,(anonymous_10)
FN:42,(anonymous_11)
FN:81,(anonymous_12)
FN:83,(anonymous_13)
FN:91,(anonymous_14)
FN:98,(anonymous_15)
FN:100,(anonymous_16)
FN:107,(anonymous_17)
FN:110,(anonymous_18)
FN:117,(anonymous_19)
FNF:15
FNH:15
FNDA:6,(anonymous_5)
FNDA:18,IceCreamShop
FNDA:18,(anonymous_7)
FNDA:18,(anonymous_8)
FNDA:18,(anonymous_9)
FNDA:52,(anonymous_10)
FNDA:132,(anonymous_11)
FNDA:5,(anonymous_12)
FNDA:5,(anonymous_13)
FNDA:3,(anonymous_14)
FNDA:2,(anonymous_15)
FNDA:2,(anonymous_16)
FNDA:2,(anonymous_17)
FNDA:2,(anonymous_18)
FNDA:5,(anonymous_19)
DA:1,6
DA:2,6
DA:8,6
DA:9,18
DA:21,18
DA:23,18
DA:24,18
DA:26,18
DA:27,18
DA:32,52
DA:33,52
DA:44,132
DA:81,6
DA:83,5
DA:84,5
DA:85,5
DA:86,5
DA:87,5
DA:91,6
DA:92,3
DA:93,1
DA:96,2
DA:97,2
DA:98,2
DA:100,2
DA:101,2
DA:102,2
DA:107,6
DA:108,2
DA:110,2
DA:111,2
DA:112,2
DA:113,2
DA:117,6
DA:118,5
DA:120,6
LF:36
LH:36
BRDA:9,0,0,18
BRDA:9,0,1,18
BRDA:41,1,0,52
BRDA:41,1,1,52
BRDA:57,2,0,52
BRDA:57,2,1,14
BRDA:73,3,0,52
BRDA:73,3,1,2
BRDA:92,4,0,1
BRDA:92,4,1,2
BRDA:118,5,0,5
BRDA:118,5,1,4
BRF:12
BRH:12
end_of_record
TN:
SF:C:\Projects\sp-dev-fx-webparts\samples\react-jest-testing\src\webparts\iceCreamShop\iceCreamProviders\IceCreamFakeProvider.ts
FN:4,(anonymous_0)
FN:6,(anonymous_2)
FN:8,(anonymous_3)
FN:21,(anonymous_4)
FN:23,(anonymous_5)
FNF:5
FNH:5
FNDA:21,(anonymous_0)
FNDA:16,(anonymous_2)
FNDA:16,(anonymous_3)
FNDA:1,(anonymous_4)
FNDA:1,(anonymous_5)
DA:4,5
DA:6,5
DA:8,16
DA:17,16
DA:21,5
DA:23,1
DA:25,5
LF:7
LH:7
BRF:0
BRH:0
end_of_record
TN:
SF:C:\Projects\sp-dev-fx-webparts\samples\react-jest-testing\src\webparts\iceCreamShop\iceCreamProviders\IceCreamPnPJsProvider.ts
FN:5,(anonymous_12)
FN:9,IceCreamPnPJsProvider
FN:13,(anonymous_14)
FN:15,(anonymous_15)
FN:23,(anonymous_18)
FN:31,(anonymous_19)
FN:35,(anonymous_20)
FN:37,(anonymous_21)
FN:42,(anonymous_22)
FN:43,(anonymous_23)
FNF:10
FNH:8
FNDA:2,(anonymous_12)
FNDA:4,IceCreamPnPJsProvider
FNDA:1,(anonymous_14)
FNDA:1,(anonymous_15)
FNDA:1,(anonymous_18)
FNDA:0,(anonymous_19)
FNDA:1,(anonymous_20)
FNDA:1,(anonymous_21)
FNDA:1,(anonymous_22)
FNDA:0,(anonymous_23)
DA:5,2
DA:10,4
DA:13,2
DA:15,1
DA:23,1
DA:25,1
DA:26,3
DA:27,3
DA:30,1
DA:31,0
DA:35,2
DA:37,1
DA:38,1
DA:42,1
DA:43,0
DA:46,2
LF:16
LH:14
BRF:0
BRH:0
end_of_record

View File

@ -0,0 +1,56 @@
<testsuites name="jest tests" tests="20" failures="0" time="16.183000000000003">
<testsuite name="Call the component methods" errors="0" failures="0" skipped="0" timestamp="2018-05-10T21:15:42" time="4.524" tests="7">
<testcase classname="Call the component methods should call fail validation if wrong state" name="Call the component methods should call fail validation if wrong state" time="0.02">
</testcase>
<testcase classname="Call the component methods should call pass validation if wrong state" name="Call the component methods should call pass validation if wrong state" time="0.002">
</testcase>
<testcase classname="Call the component methods should select the proper item when selectHandler called" name="Call the component methods should select the proper item when selectHandler called" time="0.009">
</testcase>
<testcase classname="Call the component methods should change the quantity successfully" name="Call the component methods should change the quantity successfully" time="0.004">
</testcase>
<testcase classname="Call the component methods should re-calculate new price based on quantity change" name="Call the component methods should re-calculate new price based on quantity change" time="0.005">
</testcase>
<testcase classname="Call the component methods should not show success buyHandler called by the form is not valid" name="Call the component methods should not show success buyHandler called by the form is not valid" time="0.02">
</testcase>
<testcase classname="Call the component methods should show success message after buy success" name="Call the component methods should show success message after buy success" time="0.015">
</testcase>
</testsuite>
<testsuite name="Stub pnp js to test the provider" errors="0" failures="0" skipped="0" timestamp="2018-05-10T21:15:42" time="4.872" tests="2">
<testcase classname="Stub pnp js to test the provider should return array of 3 items when sp.search called" name="Stub pnp js to test the provider should return array of 3 items when sp.search called" time="0.008">
</testcase>
<testcase classname="Stub pnp js to test the provider should pnp add new item be called" name="Stub pnp js to test the provider should pnp add new item be called" time="0.001">
</testcase>
</testsuite>
<testsuite name="Sinon stubs" errors="0" failures="0" skipped="0" timestamp="2018-05-10T21:15:42" time="4.887" tests="2">
<testcase classname="Sinon stubs should show 3 ice cream flavours" name="Sinon stubs should show 3 ice cream flavours" time="0.041">
</testcase>
<testcase classname="Sinon stubs should show success on successfull buy (e2e unit test)" name="Sinon stubs should show success on successfull buy (e2e unit test)" time="0.038">
</testcase>
</testsuite>
<testsuite name="Enzyme basics" errors="0" failures="0" skipped="0" timestamp="2018-05-10T21:15:47" time="0.319" tests="2">
<testcase classname="Enzyme basics should root web part element exists" name="Enzyme basics should root web part element exists" time="0.015">
</testcase>
<testcase classname="Enzyme basics should has the correct title" name="Enzyme basics should has the correct title" time="0.011">
</testcase>
</testsuite>
<testsuite name="Enzyme props, state, lifecycle events test" errors="0" failures="0" skipped="0" timestamp="2018-05-10T21:15:47" time="0.412" tests="4">
<testcase classname="Enzyme props, state, lifecycle events test should has test title is the props" name="Enzyme props, state, lifecycle events test should has test title is the props" time="0.049">
</testcase>
<testcase classname="Enzyme props, state, lifecycle events test should has initial state" name="Enzyme props, state, lifecycle events test should has initial state" time="0.018">
</testcase>
<testcase classname="Enzyme props, state, lifecycle events test should buy form be hidden initialy" name="Enzyme props, state, lifecycle events test should buy form be hidden initialy" time="0.012">
</testcase>
<testcase classname="Enzyme props, state, lifecycle events test should unhide the buy form after ice cream has been selected" name="Enzyme props, state, lifecycle events test should unhide the buy form after ice cream has been selected" time="0.022">
</testcase>
</testsuite>
<testsuite name="Sinon basic spy" errors="0" failures="0" skipped="0" timestamp="2018-05-10T21:15:46" time="0.829" tests="2">
<testcase classname="Sinon basic spy should handler be called once" name="Sinon basic spy should handler be called once" time="0.07">
</testcase>
<testcase classname="Sinon basic spy should handler be called with the right parameters" name="Sinon basic spy should handler be called with the right parameters" time="0.019">
</testcase>
</testsuite>
<testsuite name="Advanced selectors" errors="0" failures="0" skipped="0" timestamp="2018-05-10T21:15:47" time="0.34" tests="1">
<testcase classname="Advanced selectors should show a list of ice cream flavours" name="Advanced selectors should show a list of ice cream flavours" time="0.025">
</testcase>
</testsuite>
</testsuites>

20819
samples/react-jest-testing/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,85 @@
{
"name": "react-jest-testing",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "jest"
},
"dependencies": {
"react": "15.6.2",
"react-dom": "15.6.2",
"@types/react": "15.6.6",
"@types/react-dom": "15.5.6",
"@microsoft/sp-core-library": "~1.4.1",
"@microsoft/sp-webpart-base": "~1.4.1",
"@microsoft/sp-lodash-subset": "~1.4.1",
"@microsoft/sp-office-ui-fabric-core": "~1.4.1",
"@types/webpack-env": ">=1.12.1 <1.14.0",
"@pnp/common": "1.0.4",
"@pnp/sp": "1.0.4",
"@pnp/odata": "1.0.4",
"@pnp/logging": "1.0.4"
},
"devDependencies": {
"@microsoft/sp-build-web": "~1.4.1",
"@microsoft/sp-module-interfaces": "~1.4.1",
"@microsoft/sp-webpart-workbench": "~1.4.1",
"gulp": "~3.9.1",
"@types/chai": ">=3.4.34 <3.6.0",
"@types/mocha": ">=2.2.33 <2.6.0",
"ajv": "~5.2.2",
"@types/jest": "22.2.3",
"jest": "22.4.3",
"ts-jest": "22.4.5",
"identity-obj-proxy": "3.0.0",
"enzyme": "3.3.0",
"@types/enzyme": "3.1.10",
"enzyme-adapter-react-15": "1.0.5",
"react-test-renderer": "15.6.2",
"@types/sinon": "4.3.1",
"sinon": "5.0.7",
"jest-junit": "3.7.0"
},
"jest": {
"moduleFileExtensions": [
"ts",
"tsx",
"js"
],
"transform": {
"^.+\\.(ts|tsx)$": "ts-jest"
},
"testMatch": [
"**/src/**/*.test.+(ts|tsx|js)"
],
"mapCoverage": true,
"collectCoverage": true,
"coverageReporters": [
"json",
"lcov",
"text",
"cobertura"
],
"coverageDirectory": "<rootDir>/jest",
"moduleNameMapper": {
"\\.(css|less|scss|sass)$": "identity-obj-proxy"
},
"testResultsProcessor": "jest-junit",
"coverageThreshold": {
"global": {
"branches": 100,
"functions": 100,
"lines": 100,
"statements": 100
}
}
},
"jest-junit": {
"output": "./jest/summary-jest-junit.xml"
}
}

View File

@ -0,0 +1,19 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "d67c5975-ba3e-49ec-9a23-245c15ea3cda",
"alias": "IceCreamShopWebPart",
"componentType": "WebPart",
"version": "*",
"manifestVersion": 2,
"requiresCustomScript": false,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70",
"group": { "default": "Other" },
"title": { "default": "Ice Cream Shop" },
"description": { "default": "SPFx unit tests with Jest, Enzyme and Sinonjs" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "Ice Cream Shop"
}
}]
}

View File

@ -0,0 +1,70 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-webpart-base';
import * as strings from 'IceCreamShopWebPartStrings';
import IceCreamShop from './components/IceCreamShop';
import { IIceCreamShopProps } from './components/IIceCreamShopProps';
// import { IceCreamFakeProvider } from './iceCreamProviders/IceCreamFakeProvider'; // when offline workbench.
import { sp } from "@pnp/sp";
import { IceCreamPnPJsProvider } from './iceCreamProviders/IceCreamPnPJsProvider';
export interface IIceCreamShopWebPartProps {
description: string;
}
export default class IceCreamShopWebPart extends BaseClientSideWebPart<IIceCreamShopWebPartProps> {
public onInit(): Promise<void> {
return super.onInit().then(_ => {
sp.setup({
spfxContext: this.context
});
});
}
public render(): void {
const element: React.ReactElement<IIceCreamShopProps> = React.createElement(
IceCreamShop,
{
iceCreamProvider: new IceCreamPnPJsProvider(sp), //new IceCreamFakeProvider() // when offline workbench.
strings: strings
}
);
ReactDom.render(element, this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,6 @@
import { IIceCreamProvider } from "../iceCreamProviders/IIceCreamProvider";
export interface IIceCreamShopProps {
iceCreamProvider: IIceCreamProvider;
strings: IIceCreamShopWebPartStrings;
}

View File

@ -0,0 +1,10 @@
import { IceCream } from "../iceCreamProviders/IceCream";
export interface IIceCreamShopState {
selectedIceCream: IceCream;
quantity: number;
iceCreamFlavoursList: IceCream[];
totalPrice: number;
hasBoughtIceCream: boolean;
}

View File

@ -0,0 +1,75 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.ic {
.container {
max-width: 700px;
margin: 0px auto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.row {
@include ms-Grid-row;
@include ms-fontColor-white;
background-color: $ms-color-themeDark;
padding: 20px;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
@include ms-xlPush2;
@include ms-lgPush1;
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
padding: 5px;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,120 @@
import * as React from 'react';
import styles from './IceCreamShop.module.scss';
import { IIceCreamShopProps } from './IIceCreamShopProps';
import { escape } from '@microsoft/sp-lodash-subset';
import { IIceCreamShopState } from './IIceCreamShopState';
import { IceCream } from '../iceCreamProviders/IceCream';
export default class IceCreamShop extends React.Component<IIceCreamShopProps, IIceCreamShopState> {
constructor(props: IIceCreamShopProps) {
super(props);
this.state = {
iceCreamFlavoursList: [],
quantity: 1,
selectedIceCream: null,
totalPrice: 0,
hasBoughtIceCream: false
};
}
public componentDidMount(): void {
this.props.iceCreamProvider.getAll().then((result: IceCream[]) => {
this.setState((state: IIceCreamShopState): IIceCreamShopState => {
state.iceCreamFlavoursList = result;
return state;
});
});
}
public render(): React.ReactElement<IIceCreamShopProps> {
return (
<div className={styles.ic} id="iceCreamShop">
<div className={styles.container}>
<div className={styles.row}>
<div className={styles.column}>
<h1 className={styles.title}>{this.props.strings.TitleLabel}</h1>
<div id="iceCreamFlavoursList">
{
this.state.iceCreamFlavoursList &&
this.state.iceCreamFlavoursList.map((item, index) => {
return <div key={index}>
<div className={styles.subTitle}>{item.Title}</div>
<button data-automationid={`item-${index}`} className={styles.button} onClick={this.selectHandler.bind(this, item)}>
{this.props.strings.GetItLabel} {item.Price}
</button>
</div>;
})
}
</div>
{this.state.selectedIceCream &&
<div id="buyForm">
<div className={styles.row}>
<label className={styles.subTitle}>{this.props.strings.QuantityLabel}: </label>
<input type="number" value={this.state.quantity} min="1" onChange={this.quantityChangeHandler.bind(this)} />
</div>
<div className={styles.row}>
<button className={styles.button} id="buyButton" onClick={this.buyHandler.bind(this)}>
{this.props.strings.BuyLabel} x{this.state.quantity} {this.state.selectedIceCream.Title} {this.props.strings.ForLabel} {this.state.totalPrice}
</button>
</div>
</div>
}
{this.state.hasBoughtIceCream && <p>{this.props.strings.SuccessLabel}!</p>}
</div>
</div>
</div>
</div>
);
}
public selectHandler(iceCream: IceCream): void {
this.setState((state: IIceCreamShopState): IIceCreamShopState => {
state.selectedIceCream = iceCream;
state.totalPrice = Math.round((state.quantity * state.selectedIceCream.Price) * 100) / 100;
state.hasBoughtIceCream = false;
return state;
});
}
public buyHandler(): void {
if (this.isValid() === false) {
return;
}
const uniqueid = this.state.selectedIceCream.UniqueId;
const quantity = this.state.quantity;
this.props.iceCreamProvider.buy(uniqueid, quantity).then(result => {
this.setState((state: IIceCreamShopState): IIceCreamShopState => {
state.hasBoughtIceCream = true;
return state;
});
});
}
public quantityChangeHandler(event: React.ChangeEvent<any>) {
const inputValue = event.target.value;
this.setState((state: IIceCreamShopState): IIceCreamShopState => {
state.quantity = inputValue;
state.totalPrice = Math.round((inputValue * state.selectedIceCream.Price) * 100) / 100;
return state;
});
}
public isValid(): boolean {
return this.state.selectedIceCream !== null && this.state.quantity > 0;
}
}

View File

@ -0,0 +1,8 @@
import { IceCream } from "./IceCream";
export interface IIceCreamProvider {
getAll(): Promise<IceCream[]>;
buy(uniqueid: string, quantity: number): Promise<void>;
}

View File

@ -0,0 +1,5 @@
export interface IceCream {
UniqueId: string;
Title: string;
Price: number;
}

View File

@ -0,0 +1,25 @@
import { IceCream } from "./IceCream";
import { IIceCreamProvider } from "./IIceCreamProvider";
export class IceCreamFakeProvider implements IIceCreamProvider {
public getAll(): Promise<Array<IceCream>> {
return new Promise<Array<IceCream>>(resolve => {
let list = [
{ UniqueId: "1", Title: "Cherry" },
{ UniqueId: "2", Title: "Chocolate" },
{ UniqueId: "3", Title: "Coffee and Cookie" },
{ UniqueId: "10", Title: "Vanilla" }
] as IceCream[];
resolve(list);
});
}
public buy(uniqueid: string, quantity: number): Promise<void> {
return new Promise<void>(resolve => resolve());
}
}

View File

@ -0,0 +1,46 @@
import { IceCream } from "./IceCream";
import { IIceCreamProvider } from "./IIceCreamProvider";
import { SPRest, SearchQuery, SearchResult, SearchResults } from "@pnp/sp";
export class IceCreamPnPJsProvider implements IIceCreamProvider {
private readonly sp: SPRest;
constructor(sp: SPRest) {
this.sp = sp;
}
public getAll(): Promise<IceCream[]> {
return new Promise<IceCream[]>(async (resolve, reject) => {
const query: SearchQuery = {
RowLimit: 10,
SelectProperties: ["UniqueId", "Title", "PriceOWSNMBR"],
Querytext: 'path:https://spfxjest.sharepoint.com/sites/jest/Lists/IceCreamFlavours AND contenttypeid:0x01*'
} as SearchQuery;
this.sp.search(query).then((searchResults: SearchResults) => {
const result = [];
for (const item of searchResults.PrimarySearchResults) {
result.push({ UniqueId: item.UniqueId, Title: item.Title, Price: Math.round(item["PriceOWSNMBR"] * 100) / 100 });
}
resolve(result);
}).catch(error => reject(error));
});
}
public buy(uniqueid: string, quantity: number): Promise<any> {
return new Promise<any>((resolve, reject) => {
this.sp.web.lists.getByTitle('Ice Cream Orders').items.add({
"Title": uniqueid,
"Quantity": quantity
})
.then(result => resolve())
.catch(error => reject(error));
});
}
}

View File

@ -0,0 +1,13 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field",
"GetItLabel": "Get it for just ",
"QuantityLabel": "Quantity",
"ForLabel": "for",
"BuyLabel": "Buy",
"SuccessLabel": "Success",
"TitleLabel": "PnP Ice Cream Shop"
}
});

View File

@ -0,0 +1,16 @@
declare interface IIceCreamShopWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
TitleLabel: string;
GetItLabel: string;
QuantityLabel: string;
ForLabel: string;
BuyLabel: string;
SuccessLabel: string;
}
declare module 'IceCreamShopWebPartStrings' {
const strings: IIceCreamShopWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,59 @@
/// <reference types="jest" />
import * as React from 'react';
import { configure, mount, ReactWrapper } from 'enzyme';
import * as Adapter from 'enzyme-adapter-react-15';
configure({ adapter: new Adapter() });
import IceCreamShop from '../components/IceCreamShop';
import { IceCreamFakeProvider } from '../iceCreamProviders/IceCreamFakeProvider';
import { IIceCreamShopProps } from '../components/IIceCreamShopProps';
import { IIceCreamShopState } from '../components/IIceCreamShopState';
describe('Enzyme basics', () => {
let reactComponent: ReactWrapper<IIceCreamShopProps, IIceCreamShopState>;
beforeEach(() => {
reactComponent = mount(React.createElement(
IceCreamShop,
{
iceCreamProvider: new IceCreamFakeProvider(),
strings: {
TitleLabel: "PnP Ice Cream Shop"
} as IIceCreamShopWebPartStrings
}
));
});
afterEach(() => {
reactComponent.unmount();
});
it('should root web part element exists', () => {
// define the css selector
let cssSelector: string = '#iceCreamShop';
// find the element using css selector
const element = reactComponent.find(cssSelector);
expect(element.length).toBeGreaterThan(0);
});
it('should has the correct title', () => {
// define contains/like css selector
let cssSelector: string = 'h1';
// find the elemet using css selector
const text = reactComponent.find(cssSelector).text();
expect(text).toBe("PnP Ice Cream Shop");
});
});
// Usefull links:
// https://reactjs.org/docs/test-renderer.html
// https://github.com/airbnb/enzyme

View File

@ -0,0 +1,72 @@
/// <reference types="jest" />
import * as React from 'react';
import { configure, mount, ReactWrapper} from 'enzyme';
import * as Adapter from 'enzyme-adapter-react-15';
import { IIceCreamShopProps } from '../components/IIceCreamShopProps';
import { IIceCreamShopState } from '../components/IIceCreamShopState';
import IceCreamShop from '../components/IceCreamShop';
import { IceCreamFakeProvider } from '../iceCreamProviders/IceCreamFakeProvider';
configure({ adapter: new Adapter() });
describe('Enzyme props, state, lifecycle events test', () => {
let reactComponent: ReactWrapper<IIceCreamShopProps, IIceCreamShopState>;
beforeEach(() => {
reactComponent = mount(React.createElement(
IceCreamShop,
{
iceCreamProvider: new IceCreamFakeProvider(),
strings: {
TitleLabel: "PnP Ice Cream Shop"
} as IIceCreamShopWebPartStrings
}
));
});
afterEach(() => {
reactComponent.unmount();
});
it('should has test title is the props', () => {
expect(reactComponent.props().strings.TitleLabel).toBe("PnP Ice Cream Shop");
});
it('should has initial state', () => {
const state = reactComponent.state();
expect(state.hasBoughtIceCream).toBe(false);
expect(state.quantity).toBe(1);
expect(state.selectedIceCream).toBe(null);
});
it('should buy form be hidden initialy', () => {
const buyForm = reactComponent.find("#buyForm");
expect(buyForm.length).toBe(0);
});
it('should unhide the buy form after ice cream has been selected', () => {
reactComponent.update(); // http://airbnb.io/enzyme/docs/api/ShallowWrapper/update.html
// more advanced selector
const selectIceCreamButton = reactComponent.find("#iceCreamFlavoursList button").first();
selectIceCreamButton.simulate('click');
// after the selectIceCreamButton is clicked
// the buy form should be rendered
// lets try to find in in the component
const buyForm = reactComponent.find("#buyForm");
expect(buyForm.length).toBeGreaterThan(0);
});
});

View File

@ -0,0 +1,47 @@
/// <reference types="jest" />
import * as React from 'react';
import { configure, mount, ReactWrapper} from 'enzyme';
import * as Adapter from 'enzyme-adapter-react-15';
import { IIceCreamShopProps } from '../components/IIceCreamShopProps';
import { IIceCreamShopState } from '../components/IIceCreamShopState';
import IceCreamShop from '../components/IceCreamShop';
import { IceCreamFakeProvider } from '../iceCreamProviders/IceCreamFakeProvider';
configure({ adapter: new Adapter() });
describe('Advanced selectors', () => {
let reactComponent: ReactWrapper<IIceCreamShopProps, IIceCreamShopState>;
beforeEach(() => {
reactComponent = mount(React.createElement(
IceCreamShop,
{
iceCreamProvider: new IceCreamFakeProvider(),
strings: {} as IIceCreamShopWebPartStrings
}
));
});
afterEach(() => {
reactComponent.unmount();
});
it('should show a list of ice cream flavours', () => {
reactComponent.update();
// get the component as dom
const componentAsDOM = reactComponent.getDOMNode();
// use JavaScript querySelectorAll to find nodes that contain text
const flavours = componentAsDOM.querySelectorAll("[data-automationid*='item-']");
expect(flavours.length).toBe(4);
});
});
// Usefull links:
// https://developer.mozilla.org/en-US/docs/Web/API/Document_object_model/Locating_DOM_elements_using_selectors

View File

@ -0,0 +1,130 @@
/// <reference types="jest" />
import * as React from 'react';
import { configure, shallow, ShallowWrapper } from 'enzyme';
import * as Adapter from 'enzyme-adapter-react-15';
import { IIceCreamShopProps } from '../components/IIceCreamShopProps';
import { IIceCreamShopState } from '../components/IIceCreamShopState';
import IceCreamShop from '../components/IceCreamShop';
import { IceCreamFakeProvider } from '../iceCreamProviders/IceCreamFakeProvider';
configure({ adapter: new Adapter() });
describe('Call the component methods', () => {
let reactComponent: ShallowWrapper<IIceCreamShopProps, IIceCreamShopState>;
beforeEach(() => {
reactComponent = shallow(React.createElement(
IceCreamShop,
{
iceCreamProvider: new IceCreamFakeProvider(),
strings: {} as IIceCreamShopWebPartStrings
}
));
});
afterEach(() => {
reactComponent.unmount();
});
it('should call fail validation if wrong state', () => {
// set state
reactComponent.setState({ selectedIceCream: null, quantity: 1 });
// get the wrapper instance
const instance = reactComponent.instance() as IceCreamShop;
const isValid = instance.isValid();
expect(isValid).toBe(false);
});
it('should call pass validation if wrong state', () => {
// set state
reactComponent.setState({ selectedIceCream: { UniqueId: "123", Title: "abc", Price: 13 }, quantity: 1 });
// get the wrapper instance
const instance = reactComponent.instance() as IceCreamShop;
const isValid = instance.isValid();
expect(isValid).toBe(true);
});
it('should select the proper item when selectHandler called', () => {
// set state
reactComponent.setState({ selectedIceCream: null, quantity: 1 });
// get the wrapper instance
const instance = reactComponent.instance() as IceCreamShop;
instance.selectHandler({ UniqueId: "123", Title: "abc", Price: 13 });
expect(reactComponent.state().selectedIceCream.UniqueId).toBe("123");
expect(reactComponent.state().selectedIceCream.Title).toBe("abc");
expect(reactComponent.state().selectedIceCream.Price).toBe(13);
});
it('should change the quantity successfully', () => {
// set state
reactComponent.setState({ selectedIceCream: { UniqueId: "123", Title: "abc", Price: 2 }, quantity: 1 });
// get the wrapper instance
const instance = reactComponent.instance() as IceCreamShop;
instance.quantityChangeHandler({ target: { value: 22 } } as React.ChangeEvent<any>);
expect(reactComponent.state().quantity).toBe(22);
});
it('should re-calculate new price based on quantity change', () => {
// set state
reactComponent.setState({ selectedIceCream: { UniqueId: "123", Title: "abc", Price: 2 }, quantity: 1 });
// get the wrapper instance
const instance = reactComponent.instance() as IceCreamShop;
instance.quantityChangeHandler({ target: { value: 10 } } as React.ChangeEvent<any>);
expect(reactComponent.state().totalPrice).toBe(10 * 2); // quantity = 10, price = 2
});
it('should not show success buyHandler called by the form is not valid', (done) => {
// set state
reactComponent.setState({ selectedIceCream: { UniqueId: "123", Title: "abc", Price: 2 }, quantity: -1 });
// get the wrapper instance
const instance = reactComponent.instance() as IceCreamShop;
instance.buyHandler();
// since the buyHandler is a void method,
// we do not know when the promise inside will resolve
// this is why we have to add timeout and expect to be
// resolved after the timeout
setTimeout(() => {
expect(reactComponent.state().hasBoughtIceCream).toBe(false);
done();
}, 10);
});
it('should show success message after buy success', (done) => {
// set state
reactComponent.setState({ selectedIceCream: { UniqueId: "123", Title: "abc", Price: 2 }, quantity: 1 });
// get the wrapper instance
const instance = reactComponent.instance() as IceCreamShop;
instance.buyHandler();
// since the buyHandler is a void method,
// we do not know when the promise inside will resolve
// this is why we have to add timeout and expect to be
// resolved after the timeout
setTimeout(() => {
expect(reactComponent.state().hasBoughtIceCream).toBe(true);
done();
}, 10);
});
});

View File

@ -0,0 +1,75 @@
/// <reference types="jest" />
import * as React from 'react';
import { configure, mount, ReactWrapper } from 'enzyme';
import * as Adapter from 'enzyme-adapter-react-15';
import { IIceCreamShopProps } from '../components/IIceCreamShopProps';
import { IIceCreamShopState } from '../components/IIceCreamShopState';
import IceCreamShop from '../components/IceCreamShop';
import { IceCreamFakeProvider } from '../iceCreamProviders/IceCreamFakeProvider';
import * as sinon from 'sinon';
configure({ adapter: new Adapter() });
describe('Sinon basic spy', () => {
let reactComponent: ReactWrapper<IIceCreamShopProps, IIceCreamShopState>;
let selectHandlerSpy: sinon.SinonSpy;
beforeEach(() => {
// set spy on the select handler
selectHandlerSpy = sinon.spy(IceCreamShop.prototype, 'selectHandler');
// mount the component
reactComponent = mount(React.createElement(
IceCreamShop,
{
iceCreamProvider: new IceCreamFakeProvider(),
strings: {} as IIceCreamShopWebPartStrings
}
));
});
afterEach(() => {
reactComponent.unmount();
selectHandlerSpy.restore();
});
it('should handler be called once', () => {
reactComponent.update();
// more advanced selector
const selectIceCreamButton = reactComponent.find("#iceCreamFlavoursList button").first();
selectIceCreamButton.simulate('click');
// after the selectIceCreamButton is clicked
// the buy form should be rendered
// lets try to find in in the component
const buyForm = reactComponent.find("#buyForm");
expect(selectHandlerSpy.calledOnce).toBe(true);
});
it('should handler be called with the right parameters', () => {
reactComponent.update();
// more advanced selector
const selectIceCreamButton = reactComponent.find("#iceCreamFlavoursList button").first();
selectIceCreamButton.simulate('click');
// after the selectIceCreamButton is clicked
// the buy form should be rendered
// lets try to find in in the component
const buyForm = reactComponent.find("#buyForm");
expect(selectHandlerSpy.calledWith({ UniqueId: "1", Title: "Cherry" })).toBe(true);
});
});
// http://sinonjs.org/

View File

@ -0,0 +1,133 @@
/// <reference types="jest" />
import * as React from 'react';
import { configure, mount, ReactWrapper } from 'enzyme';
import * as Adapter from 'enzyme-adapter-react-15';
import { IIceCreamShopProps } from '../components/IIceCreamShopProps';
import { IIceCreamShopState } from '../components/IIceCreamShopState';
import IceCreamShop from '../components/IceCreamShop';
import { IceCreamPnPJsProvider } from '../iceCreamProviders/IceCreamPnPJsProvider';
import { IceCream } from '../iceCreamProviders/IceCream';
import { sp } from "@pnp/sp";
import * as sinon from 'sinon';
configure({ adapter: new Adapter() });
describe('Sinon stubs', () => {
let reactComponent: ReactWrapper<IIceCreamShopProps, IIceCreamShopState>;
let iceCreamPnPJsProviderGetAllStub: sinon.SinonStub;
let iceCreamPnPJsProviderBuyStub: sinon.SinonStub;
beforeEach(() => {
// set stubs on the pnp js provider so it does not call SharePoint at all
iceCreamPnPJsProviderGetAllStub = sinon.stub(IceCreamPnPJsProvider.prototype, "getAll");
iceCreamPnPJsProviderBuyStub = sinon.stub(IceCreamPnPJsProvider.prototype, "buy");
});
afterEach(() => {
reactComponent.unmount();
iceCreamPnPJsProviderGetAllStub.restore();
iceCreamPnPJsProviderBuyStub.restore();
});
it('should show 3 ice cream flavours', (done) => {
// mocks the promise and resolves it returnig
// 3 items, no https call is made.
iceCreamPnPJsProviderGetAllStub.resolves([
{ UniqueId: "GUID1", Title: "Cherry", Price: 1 },
{ UniqueId: "GUID2", Title: "Chocolate", Price: 2 },
{ UniqueId: "GUID3", Title: "Coffee and Cookie", Price: 3 }
] as IceCream[]);
// mount the component
// componentDidMount will be called
// and the component should recieve the data above
reactComponent = mount(React.createElement(
IceCreamShop,
{
iceCreamProvider: new IceCreamPnPJsProvider(sp),
strings: {} as IIceCreamShopWebPartStrings
}
));
// since the componentDidMount is a lifecycle event,
// we do not know when the promise inside will resolve
// this is why we have to add timeout and expect to be
// resolved after the timeout
setTimeout(() => {
// check if the state is populated with the data
const items = reactComponent.state().iceCreamFlavoursList;
expect(items.length).toBe(3);
done();
}, 0);
});
it('should show success on successfull buy (e2e unit test)', (done) => {
// mocks the promise and resolves it returnig
// 3 items, no https call is made.
iceCreamPnPJsProviderGetAllStub.resolves([
{ UniqueId: "GUID1", Title: "Cherry", Price: 1 },
{ UniqueId: "GUID2", Title: "Chocolate", Price: 2 },
{ UniqueId: "GUID3", Title: "Coffee and Cookie", Price: 3 }
] as IceCream[]);
// mocks buy success ignoring any calls
// over https
iceCreamPnPJsProviderBuyStub.resolves();
// mount the component
reactComponent = mount(React.createElement(
IceCreamShop,
{
iceCreamProvider: new IceCreamPnPJsProvider(sp),
strings: {
TitleLabel: "PnP Ice Cream Shop"
} as IIceCreamShopWebPartStrings
}
));
// we have to wait componenDidMount to load
// the fake data
setTimeout(() => {
reactComponent.update();
// find first ice cream flavour and select it
const selectIceCreamButton = reactComponent.find("#iceCreamFlavoursList button").first();
selectIceCreamButton.simulate('click');
reactComponent.update();
// find the buy button and buy it
const buyButton = reactComponent.find("#buyButton").first();
buyButton.simulate('click');
// since the buyHandler is a void method,
// we do not know when the promise inside will resolve
// this is why we have to add timeout and expect to be
// resolved after the timeout
setTimeout(() => {
// check if the state is populated with the data
const items = reactComponent.state().iceCreamFlavoursList;
expect(reactComponent.state().hasBoughtIceCream).toBe(true);
done();
}, 0);
});
}, 0);
});
// http://sinonjs.org/
// Remarks: In general the last test has two timeouts due void handlers
// of the react component handlers. It has to wait the first time for
// componentDidMount to load the ice creams list, but then has to wait
// with setTimeout for the buyHandler to complete because it is void.
/// If the buyHandler was promise, then this would remove
// the need of a second timeout function.

View File

@ -0,0 +1,84 @@
/// <reference types="jest" />
import * as React from 'react';
import { configure, mount, ReactWrapper } from 'enzyme';
import * as Adapter from 'enzyme-adapter-react-15';
import { IceCreamPnPJsProvider } from '../iceCreamProviders/IceCreamPnPJsProvider';
import { IceCream } from '../iceCreamProviders/IceCream';
import { sp, SearchResults, Items } from "@pnp/sp";
import * as sinon from 'sinon';
configure({ adapter: new Adapter() });
describe('Stub pnp js to test the provider', () => {
let myPnPJsProvider: IceCreamPnPJsProvider;
let pnpSearchStub: sinon.SinonStub;
let pnpItemsAddStub: sinon.SinonStub;
let pnpListGetByTitleStub: sinon.SinonStub;
beforeEach(() => {
// set stubs on the pnp js mathods
pnpSearchStub = sinon.stub(sp, "search");
pnpListGetByTitleStub = sinon.stub(sp.web.lists, "getByTitle");
pnpItemsAddStub = sinon.stub(Items.prototype, "add");
// create instance of the pnp js provider to test it
myPnPJsProvider = new IceCreamPnPJsProvider(sp);
});
afterEach(() => {
pnpSearchStub.restore();
pnpListGetByTitleStub.restore();
pnpItemsAddStub.restore();
});
it('should return array of 3 items when sp.search called', (done) => {
// mocks the search api so it returns exactly whan we
// want and no https calls are made
pnpSearchStub.resolves({
PrimarySearchResults: [
{ UniqueId: "GUID1", Title: "Cherry", PriceOWSNMBR: 1 },
{ UniqueId: "GUID2", Title: "Chocolate", PriceOWSNMBR: 2 },
{ UniqueId: "GUID3", Title: "Coffee and Cookie", PriceOWSNMBR: 3 }
]
});
// call the stub and see if the provider works as expected
myPnPJsProvider.getAll()
.then((result: IceCream[]) => {
expect(result.length).toBeGreaterThan(0);
done();
}).catch(e => {
done.fail(new Error('Filed to retrieve data.'));
// done();
});
});
it('should pnp add new item be called', (done) => {
// mocks the sp.list.getByTitle api so it resolves with success
pnpListGetByTitleStub.resolves();
// mocks the sp...items.add api so it resolves with success
pnpItemsAddStub.resolves();
// test my pnp provider now
myPnPJsProvider.buy("123", 1)
.then(result => {
expect(result).toBe(undefined); // since buy is void method
done();
}).catch(e => {
done.fail(new Error('Filed to retrieve data.'));
// done();
});
});
});
// https://facebook.github.io/jest/docs/en/tutorial-async.html

View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "commonjs",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
]
}
}