Initial commit
This commit is contained in:
parent
fa0c6059d0
commit
a08f327fe4
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"@microsoft/generator-sharepoint": {
|
||||||
|
"plusBeta": true,
|
||||||
|
"isCreatingSolution": true,
|
||||||
|
"environment": "spo",
|
||||||
|
"whichFolder": "subdir"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Build generated files
|
||||||
|
dist
|
||||||
|
lib
|
||||||
|
release
|
||||||
|
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
|
|
@ -0,0 +1,16 @@
|
||||||
|
!dist
|
||||||
|
config
|
||||||
|
|
||||||
|
gulpfile.js
|
||||||
|
|
||||||
|
release
|
||||||
|
src
|
||||||
|
temp
|
||||||
|
|
||||||
|
tsconfig.json
|
||||||
|
tslint.json
|
||||||
|
|
||||||
|
*.log
|
||||||
|
|
||||||
|
.yo-rc.json
|
||||||
|
.vscode
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"@microsoft/generator-sharepoint": {
|
||||||
|
"version": "1.13.0-beta.17",
|
||||||
|
"libraryName": "react-graph-auto-batching",
|
||||||
|
"libraryId": "03631505-3899-4de0-bebe-f59283d991ba",
|
||||||
|
"environment": "spo",
|
||||||
|
"packageManager": "npm",
|
||||||
|
"isCreatingSolution": true,
|
||||||
|
"plusBeta": true,
|
||||||
|
"isDomainIsolated": false,
|
||||||
|
"componentType": "webpart"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
# react-graph-auto-batching
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
This same shows how to abstract batching graph requests. The idea behind this sample is quite simple. To render simple user card we need three calls to MS GraphAPI.
|
||||||
|
First one to get user information, second to get the image and third to get the presence. As with rendering multiple user card we may hit graph throttling,
|
||||||
|
it is a good idea to batch those requests. This might create quite a repetition across our solution.
|
||||||
|
To avoid such situation, we can implement composition pattern to abstract that batch generation. This design pattern will also help us with unit testing.
|
||||||
|
|
||||||
|
This is the second idea behind this sample. It provides few unit tests of quite a complex implementation detail, which auto batching is.
|
||||||
|
You can also find how one can test react component in SPFx stack in isolation, by providing mock to all data access layer implementation.
|
||||||
|
|
||||||
|
Finally, let me know if You would like to see this auto batcher in some library You can import to Your solution.
|
||||||
|
|
||||||
|
You may ask why I used AadHttpClient instead of MSGraphClient. AadHttpClient shares more similarity with a http client based on fetch api, this means it will be easier to adopt this solution to an implementation outside of SPFx.
|
||||||
|
|
||||||
|
## Used SharePoint Framework Version
|
||||||
|
|
||||||
|
![version](https://img.shields.io/badge/version-1.13-green.svg)
|
||||||
|
|
||||||
|
## Applies to
|
||||||
|
|
||||||
|
- [SharePoint Framework](https://aka.ms/spfx)
|
||||||
|
- [Microsoft 365 tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
|
||||||
|
|
||||||
|
> Get your own free development tenant by subscribing to
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
None
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
Solution|Author(s)
|
||||||
|
--------|---------
|
||||||
|
react-graph-auto-batching | Marcin Wojciechowski [@mgwojciech](https://twitter.com/mgwojciech)
|
||||||
|
|
||||||
|
## Version history
|
||||||
|
|
||||||
|
Version|Date|Comments
|
||||||
|
-------|----|--------
|
||||||
|
1.0|February 3, 2022|Initial release
|
||||||
|
|
||||||
|
## 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
|
||||||
|
- Ensure that you are at the solution folder
|
||||||
|
- in the command-line run:
|
||||||
|
- **npm install**
|
||||||
|
- **gulp serve**
|
||||||
|
- Add Graph Auto Batching
|
||||||
|
|
||||||
|
To run tests
|
||||||
|
Clone this repository
|
||||||
|
- Ensure that you are at the solution folder
|
||||||
|
- in the command-line run:
|
||||||
|
- **npm install**
|
||||||
|
- **npx jest**
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
Description of the extension that expands upon high-level summary above.
|
||||||
|
|
||||||
|
This extension illustrates the following concepts:
|
||||||
|
|
||||||
|
- User Card using auto batching requests to MS Graph Client
|
||||||
|
- Unit testing batching client using mocked http client
|
||||||
|
- Unit testing React component in SPFx
|
||||||
|
- In total isolation
|
||||||
|
- Awaiting the **useEffect(()=>{},[])** operation
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Getting started with SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
|
||||||
|
- [Building for Microsoft teams](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/build-for-teams-overview)
|
||||||
|
- [Use Microsoft Graph in your solution](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/using-microsoft-graph-apis)
|
||||||
|
- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/publish-to-marketplace-overview)
|
||||||
|
- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development
|
||||||
|
- You can find more on unit testing on my [Blog](https://mgwdevcom.wordpress.com)
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
|
||||||
|
"version": "2.0",
|
||||||
|
"bundles": {
|
||||||
|
"graph-auto-batching-web-part": {
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"entrypoint": "./lib/webparts/graphAutoBatching/GraphAutoBatchingWebPart.js",
|
||||||
|
"manifest": "./src/webparts/graphAutoBatching/GraphAutoBatchingWebPart.manifest.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"externals": {},
|
||||||
|
"localizedResources": {
|
||||||
|
"GraphAutoBatchingWebPartStrings": "lib/webparts/graphAutoBatching/loc/{locale}.js"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
|
||||||
|
"deployCdnPath": "./release/assets/"
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
|
||||||
|
"workingDir": "./release/assets/",
|
||||||
|
"account": "<!-- STORAGE ACCOUNT NAME -->",
|
||||||
|
"container": "react-graph-auto-batching",
|
||||||
|
"accessKey": "<!-- ACCESS KEY -->"
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
|
||||||
|
"solution": {
|
||||||
|
"name": "react-graph-auto-batching-client-side-solution",
|
||||||
|
"id": "03631505-3899-4de0-bebe-f59283d991ba",
|
||||||
|
"version": "1.0.0.0",
|
||||||
|
"includeClientSideAssets": true,
|
||||||
|
"skipFeatureDeployment": true,
|
||||||
|
"isDomainIsolated": false,
|
||||||
|
"developer": {
|
||||||
|
"name": "",
|
||||||
|
"websiteUrl": "",
|
||||||
|
"privacyUrl": "",
|
||||||
|
"termsOfUseUrl": "",
|
||||||
|
"mpnId": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"paths": {
|
||||||
|
"zippedPackage": "solution/react-graph-auto-batching.sppkg"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
|
||||||
|
"port": 4321,
|
||||||
|
"https": true,
|
||||||
|
"initialPage": "https://enter-your-SharePoint-site/_layouts/workbench.aspx"
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||||
|
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
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.`);
|
||||||
|
|
||||||
|
var getTasks = build.rig.getTasks;
|
||||||
|
build.rig.getTasks = function () {
|
||||||
|
var result = getTasks.call(build.rig);
|
||||||
|
|
||||||
|
result.set('serve', result.get('serve-deprecated'));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
build.initialize(require('gulp'));
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,46 @@
|
||||||
|
{
|
||||||
|
"name": "react-graph-auto-batching",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "gulp bundle",
|
||||||
|
"clean": "gulp clean",
|
||||||
|
"test": "gulp test"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "16.9.0",
|
||||||
|
"react-dom": "16.9.0",
|
||||||
|
"office-ui-fabric-react": "7.156.0",
|
||||||
|
"@microsoft/sp-core-library": "1.13.0-beta.17",
|
||||||
|
"@microsoft/sp-property-pane": "1.13.0-beta.17",
|
||||||
|
"@microsoft/sp-webpart-base": "1.13.0-beta.17",
|
||||||
|
"@microsoft/sp-lodash-subset": "1.13.0-beta.17",
|
||||||
|
"@microsoft/sp-office-ui-fabric-core": "1.13.0-beta.17"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@microsoft/rush-stack-compiler-3.7": "0.2.3",
|
||||||
|
"@microsoft/sp-build-web": "1.13.0-beta.17",
|
||||||
|
"@microsoft/sp-module-interfaces": "1.13.0-beta.17",
|
||||||
|
"@microsoft/sp-tslint-rules": "1.13.0-beta.17",
|
||||||
|
"@testing-library/react": "^12.1.2",
|
||||||
|
"@types/chai": "^4.3.0",
|
||||||
|
"@types/jest": "^27.4.0",
|
||||||
|
"@types/react": "16.9.36",
|
||||||
|
"@types/react-dom": "16.9.8",
|
||||||
|
"@types/webpack-env": "1.13.1",
|
||||||
|
"ajv": "~5.2.2",
|
||||||
|
"chai": "^4.3.6",
|
||||||
|
"gulp": "~4.0.2",
|
||||||
|
"jest": "^27.4.7",
|
||||||
|
"jest-environment-jsdom": "^27.4.6",
|
||||||
|
"jsdom": "^19.0.0",
|
||||||
|
"ts-jest": "^27.1.3"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(ts|tsx)$": "ts-jest"
|
||||||
|
},
|
||||||
|
"testEnvironment": "jsdom"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { ArrayUtilities } from "../../utils/ArrayUtilities";
|
||||||
|
import { IHttpClient, IHttpClientResponse } from "./IHttpClient";
|
||||||
|
import { BatchHandler } from "./BatchHandler";
|
||||||
|
|
||||||
|
export class BatchGraphClient implements IHttpClient {
|
||||||
|
private batch: {
|
||||||
|
id: string;
|
||||||
|
url: string;
|
||||||
|
method: "GET";
|
||||||
|
headers: any;
|
||||||
|
}[] = [];
|
||||||
|
private registeredPromises: Map<string, { resolve, error }[]> = new Map<string, { resolve, error }[]>();
|
||||||
|
constructor(protected baseClient: IHttpClient, public batchWaitTime = 500, public batchSplitThreshold = 15) {
|
||||||
|
}
|
||||||
|
public get(url: string, options?): Promise<IHttpClientResponse> {
|
||||||
|
return new Promise<IHttpClientResponse>((resolve, error) => {
|
||||||
|
this.createGetBatchRequest(url, { resolve, error });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public post(url: string, options?): Promise<IHttpClientResponse> {
|
||||||
|
return this.baseClient.post(url, options);
|
||||||
|
}
|
||||||
|
public patch(url: string, options?): Promise<IHttpClientResponse> {
|
||||||
|
return this.baseClient.patch(url, options);
|
||||||
|
}
|
||||||
|
public put(url: string, options?): Promise<IHttpClientResponse> {
|
||||||
|
return this.baseClient.put(url, options);
|
||||||
|
}
|
||||||
|
public delete(url: string): Promise<IHttpClientResponse> {
|
||||||
|
return this.baseClient.delete(url);
|
||||||
|
}
|
||||||
|
protected generateBatch = async () => {
|
||||||
|
const requestBatch = [...this.batch];
|
||||||
|
this.batch = [];
|
||||||
|
const requestPromises = new Map<string, { resolve, error }[]>(this.registeredPromises as any);
|
||||||
|
|
||||||
|
this.registeredPromises.clear();
|
||||||
|
|
||||||
|
//As there is an limit to max batch size (15) let's split our request to sub batches we will run sequentially
|
||||||
|
let batches = ArrayUtilities.splitToMaxLength(requestBatch, this.batchSplitThreshold);
|
||||||
|
for (const batch of batches) {
|
||||||
|
let promisesToBeResolvedByCurrentBatch = ArrayUtilities.getSubMap(requestPromises, batch.map(b => b.id));
|
||||||
|
const batchHandler = new BatchHandler(this.baseClient, promisesToBeResolvedByCurrentBatch, batch, BatchHandler.maxRetries);
|
||||||
|
await batchHandler.executeBatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public createGetBatchRequest = (url: string, requestPromise: { resolve, error }) => {
|
||||||
|
if (this.batch.length === 0) {
|
||||||
|
setTimeout(this.generateBatch, this.batchWaitTime);
|
||||||
|
}
|
||||||
|
let promiseId = encodeURIComponent(url);
|
||||||
|
if (this.batch.filter(req => req.id === promiseId)[0]) {
|
||||||
|
this.registeredPromises.get(url).push(requestPromise);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.batch.push({
|
||||||
|
url,
|
||||||
|
id: promiseId,
|
||||||
|
method: "GET",
|
||||||
|
headers:{
|
||||||
|
"ConsistencyLevel":"eventual"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.registeredPromises.set(promiseId, [requestPromise]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { IHttpClient } from "./IHttpClient";
|
||||||
|
|
||||||
|
export class BatchHandler {
|
||||||
|
public static readonly maxRetries = 5;
|
||||||
|
constructor(protected baseClient: IHttpClient, protected registeredPromises: Map<string, { resolve, error }[]>, protected batch: { id: string; url: string; method: "GET" }[], protected retries: number = 0) { }
|
||||||
|
|
||||||
|
public async executeBatch() {
|
||||||
|
let betaRequests = this.batch.filter(req => req.url.indexOf("v1.0/") < 0);
|
||||||
|
let v1Requests = this.batch.filter(req => req.url.indexOf("v1.0/") >= 0);
|
||||||
|
let batchBody = JSON.stringify({ requests: betaRequests });
|
||||||
|
let batchBodyV1 = JSON.stringify({
|
||||||
|
requests: v1Requests.map(req => ({
|
||||||
|
...req,
|
||||||
|
url: req.url.replace("v1.0/", "")
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
let responses = [];
|
||||||
|
if (betaRequests.length > 0) {
|
||||||
|
let betaResponse = await this.requestBatch(batchBody);
|
||||||
|
responses.push(...betaResponse.responses);
|
||||||
|
}
|
||||||
|
if (v1Requests.length > 0) {
|
||||||
|
let v1Response = await this.requestBatch(batchBodyV1, "v1.0");
|
||||||
|
responses.push(...v1Response.responses);
|
||||||
|
}
|
||||||
|
this.processBatchResponse(responses);
|
||||||
|
if (this.batch.length > 0 && this.retries > 0) {
|
||||||
|
this.retries--;
|
||||||
|
this.executeBatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private processBatchResponse(responses) {
|
||||||
|
const retryBatch = [];
|
||||||
|
const retryRegisteredPromises: Map<string, { resolve, error }[]> = new Map<string, { resolve, error }[]>();
|
||||||
|
|
||||||
|
this.registeredPromises.forEach((promises: { resolve; error; }[], url: string) => {
|
||||||
|
let promiseResponse = responses.filter(resp => resp.id === url)[0];
|
||||||
|
if (promiseResponse && promiseResponse.status === 429 && this.retries > 0) {
|
||||||
|
retryBatch.push({
|
||||||
|
url,
|
||||||
|
id: url,
|
||||||
|
method: "GET"
|
||||||
|
});
|
||||||
|
retryRegisteredPromises.set(url, promises);
|
||||||
|
} else {
|
||||||
|
this.handleSingleResponse(promiseResponse, promises);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (retryBatch.length > 0) {
|
||||||
|
this.registeredPromises = retryRegisteredPromises;
|
||||||
|
this.batch = retryBatch;
|
||||||
|
} else {
|
||||||
|
this.batch = [];
|
||||||
|
this.registeredPromises.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleSingleResponse(promiseResponse: any, promises: { resolve: any; error: any; }[]) {
|
||||||
|
if (promiseResponse) {
|
||||||
|
promises.forEach(promise => {
|
||||||
|
promise.resolve({
|
||||||
|
json: () => Promise.resolve(promiseResponse.body),
|
||||||
|
ok: promiseResponse.status === 200,
|
||||||
|
text: () => Promise.resolve(JSON.stringify(promiseResponse.body))
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
promises.forEach(promise => {
|
||||||
|
promise.error({
|
||||||
|
json: () => Promise.resolve(promiseResponse),
|
||||||
|
ok: false,
|
||||||
|
text: () => Promise.resolve(JSON.stringify(promiseResponse))
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected requestBatch = async (batchBody: string, version: string = "beta") => {
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
ConsistencyLevel: "eventual",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}, body: batchBody
|
||||||
|
};
|
||||||
|
const response = await this.baseClient.post(`https://graph.microsoft.com/${version}/$batch`, options);
|
||||||
|
if (response.ok) {
|
||||||
|
let batchResponse = await response.json();
|
||||||
|
return batchResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
export interface IHttpClientResponse {
|
||||||
|
json: () => Promise<any>;
|
||||||
|
text: () => Promise<string>;
|
||||||
|
blob: () => Promise<Blob>;
|
||||||
|
ok: boolean;
|
||||||
|
status: number;
|
||||||
|
statusText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IHttpClient {
|
||||||
|
get(url: string, options?: RequestInit): Promise<IHttpClientResponse>;
|
||||||
|
post(url: string, options?: RequestInit): Promise<IHttpClientResponse>;
|
||||||
|
patch(url: string, options?: RequestInit): Promise<IHttpClientResponse>;
|
||||||
|
put(url: string, options?: RequestInit): Promise<IHttpClientResponse>;
|
||||||
|
delete(url: string): Promise<IHttpClientResponse>;
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { IHttpClient, IHttpClientResponse } from "./IHttpClient";
|
||||||
|
import { AadHttpClient } from "@microsoft/sp-http";
|
||||||
|
|
||||||
|
export class SPFxHttpClient implements IHttpClient {
|
||||||
|
constructor(protected httpClient: AadHttpClient) {
|
||||||
|
|
||||||
|
}
|
||||||
|
public get(url: string, options?: RequestInit): Promise<IHttpClientResponse> {
|
||||||
|
return this.httpClient.get(url, AadHttpClient.configurations.v1, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
...options?.headers,
|
||||||
|
Accept: "application/json",
|
||||||
|
ConsistencyLevel: "eventual",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public post(url: string, options?: RequestInit): Promise<IHttpClientResponse> {
|
||||||
|
return this.httpClient.post(url, AadHttpClient.configurations.v1, options);
|
||||||
|
}
|
||||||
|
public patch(url: string, options?: RequestInit): Promise<IHttpClientResponse> {
|
||||||
|
return this.httpClient.fetch(url, AadHttpClient.configurations.v1, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
...options?.headers,
|
||||||
|
Accept: "application/json",
|
||||||
|
ConsistencyLevel: "eventual",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
method: "PATCH"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public put(url: string, options?: RequestInit): Promise<IHttpClientResponse> {
|
||||||
|
return this.httpClient.fetch(url, AadHttpClient.configurations.v1, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
...options?.headers,
|
||||||
|
Accept: "application/json",
|
||||||
|
ConsistencyLevel: "eventual",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
method: "PUT"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public delete(url: string): Promise<IHttpClientResponse> {
|
||||||
|
return this.httpClient.fetch(url, AadHttpClient.configurations.v1, {
|
||||||
|
method: "DELETE"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
|
@ -0,0 +1,16 @@
|
||||||
|
export class ArrayUtilities {
|
||||||
|
public static splitToMaxLength<T>(arr: T[], length: number): T[][] {
|
||||||
|
let result = [];
|
||||||
|
let startIndex = 0;
|
||||||
|
while (startIndex < arr.length) {
|
||||||
|
result.push(arr.slice(startIndex, startIndex + length));
|
||||||
|
startIndex += length;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
public static getSubMap<T, U>(map: Map<T, U>, keys: T[]): Map<T, U> {
|
||||||
|
let result = new Map<T, U>();
|
||||||
|
keys.forEach(key => result.set(key, map.get(key)));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { PersonaPresence } from "office-ui-fabric-react";
|
||||||
|
|
||||||
|
export class StringUtilities {
|
||||||
|
public static getInitials(fullName: string) {
|
||||||
|
if (!fullName) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
let initials = "";
|
||||||
|
let names = fullName.split(" ");
|
||||||
|
for (let i = 0; i < names.length && i < 2; i++) {
|
||||||
|
let name = names[i];
|
||||||
|
if (name) {
|
||||||
|
initials += name.charAt(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return initials.toUpperCase();
|
||||||
|
}
|
||||||
|
public static getPresence(presenceString): PersonaPresence{
|
||||||
|
switch (presenceString) {
|
||||||
|
case "":
|
||||||
|
return PersonaPresence.none;
|
||||||
|
case "Available":
|
||||||
|
return PersonaPresence.online;
|
||||||
|
case "Busy":
|
||||||
|
return PersonaPresence.busy;
|
||||||
|
default:
|
||||||
|
return PersonaPresence.offline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
|
||||||
|
"id": "4e7a2f9d-90f1-4896-bc61-e844f0dde102",
|
||||||
|
"alias": "GraphAutoBatchingWebPart",
|
||||||
|
"componentType": "WebPart",
|
||||||
|
|
||||||
|
// The "*" signifies that the version should be taken from the package.json
|
||||||
|
"version": "*",
|
||||||
|
"manifestVersion": 2,
|
||||||
|
|
||||||
|
// If true, the component can only be installed on sites where Custom Script is allowed.
|
||||||
|
// Components that allow authors to embed arbitrary script code should set this to true.
|
||||||
|
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
|
||||||
|
"requiresCustomScript": false,
|
||||||
|
"supportedHosts": ["SharePointWebPart"],
|
||||||
|
|
||||||
|
"preconfiguredEntries": [{
|
||||||
|
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
|
||||||
|
"group": { "default": "Other" },
|
||||||
|
"title": { "default": "Graph Auto Batching" },
|
||||||
|
"description": { "default": "Graph Auto Batching description" },
|
||||||
|
"officeFabricIconFontName": "Page",
|
||||||
|
"properties": {
|
||||||
|
"description": "Graph Auto Batching"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as ReactDom from 'react-dom';
|
||||||
|
import { Version } from '@microsoft/sp-core-library';
|
||||||
|
import {
|
||||||
|
IPropertyPaneConfiguration,
|
||||||
|
PropertyPaneTextField
|
||||||
|
} from '@microsoft/sp-property-pane';
|
||||||
|
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
|
||||||
|
|
||||||
|
import * as strings from 'GraphAutoBatchingWebPartStrings';
|
||||||
|
import GraphAutoBatching from './components/GraphAutoBatching';
|
||||||
|
import { IGraphAutoBatchingProps } from './components/IGraphAutoBatchingProps';
|
||||||
|
import { IHttpClient } from '../../dal/http/IHttpClient';
|
||||||
|
import { SPFxHttpClient } from '../../dal/http/SPFxHttpClient';
|
||||||
|
import { BatchGraphClient } from '../../dal/http/BatchGraphClient';
|
||||||
|
|
||||||
|
export interface IGraphAutoBatchingWebPartProps {
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class GraphAutoBatchingWebPart extends BaseClientSideWebPart<IGraphAutoBatchingWebPartProps> {
|
||||||
|
protected httpClient: IHttpClient;
|
||||||
|
|
||||||
|
protected async onInit(): Promise<void> {
|
||||||
|
let client = await this.context.aadHttpClientFactory.getClient('https://graph.microsoft.com');
|
||||||
|
this.httpClient = new BatchGraphClient(new SPFxHttpClient(client));
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): void {
|
||||||
|
const element: React.ReactElement<IGraphAutoBatchingProps> = React.createElement(
|
||||||
|
GraphAutoBatching,
|
||||||
|
{
|
||||||
|
graphClient: this.httpClient
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
ReactDom.render(element, this.domElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onDispose(): void {
|
||||||
|
ReactDom.unmountComponentAtNode(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
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
@import '~office-ui-fabric-react/dist/sass/References.scss';
|
||||||
|
|
||||||
|
.graphAutoBatching {
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import styles from './GraphAutoBatching.module.scss';
|
||||||
|
import { IGraphAutoBatchingProps } from './IGraphAutoBatchingProps';
|
||||||
|
import { UserCard } from './UserCard';
|
||||||
|
|
||||||
|
export default class GraphAutoBatching extends React.Component<IGraphAutoBatchingProps, {}> {
|
||||||
|
public render(): React.ReactElement<IGraphAutoBatchingProps> {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<UserCard graphClient={this.props.graphClient} userQuery={"/me"} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<UserCard graphClient={this.props.graphClient} userQuery={"/me/manager"} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { IHttpClient } from "../../../dal/http/IHttpClient";
|
||||||
|
|
||||||
|
export interface IGraphAutoBatchingProps {
|
||||||
|
graphClient: IHttpClient;
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { Spinner } from "office-ui-fabric-react";
|
||||||
|
import * as React from "react";
|
||||||
|
import { IHttpClient } from "../../../dal/http/IHttpClient";
|
||||||
|
import { IPersonaSharedProps, Persona, PersonaSize, PersonaPresence } from "office-ui-fabric-react";
|
||||||
|
import { StringUtilities } from "../../../utils/StringUtilities";
|
||||||
|
|
||||||
|
export interface IUserCardProps{
|
||||||
|
graphClient: IHttpClient;
|
||||||
|
userQuery: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UserCard(props: IUserCardProps){
|
||||||
|
const [user, setUser] = React.useState<any>({});
|
||||||
|
const [loading, setLoading] = React.useState(true);
|
||||||
|
const getUserInfo = async ()=>{
|
||||||
|
const [userInfoRequest, userPhotoRequest, presenceInfo] = await Promise.all([props.graphClient.get(props.userQuery),
|
||||||
|
props.graphClient.get(props.userQuery + "/photo/$value"),
|
||||||
|
props.graphClient.get(props.userQuery + "/presence")]);
|
||||||
|
const [userResult, photo, presence] = await Promise.all([userInfoRequest.json(), userPhotoRequest.text(),presenceInfo.json()]);
|
||||||
|
setUser({
|
||||||
|
...userResult,
|
||||||
|
presence: presence.availability,
|
||||||
|
photo: photo.replace("\"","").replace("\"","")
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
React.useEffect(() => {
|
||||||
|
getUserInfo();
|
||||||
|
}, [props.userQuery]);
|
||||||
|
|
||||||
|
if(loading){
|
||||||
|
return <Spinner />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Persona
|
||||||
|
imageUrl={user.photo}
|
||||||
|
imageInitials={StringUtilities.getInitials(user.displayName)}
|
||||||
|
text={user.displayName}
|
||||||
|
secondaryText={user.jobTitle}
|
||||||
|
tertiaryText={user.presence}
|
||||||
|
presence={StringUtilities.getPresence(user.presence)}
|
||||||
|
size={PersonaSize.size100}
|
||||||
|
imageAlt="Annie Lindqvist, status is blocked"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
7
samples/react-graph-auto-batching/src/webparts/graphAutoBatching/loc/en-us.js
vendored
Normal file
7
samples/react-graph-auto-batching/src/webparts/graphAutoBatching/loc/en-us.js
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
define([], function() {
|
||||||
|
return {
|
||||||
|
"PropertyPaneDescription": "Description",
|
||||||
|
"BasicGroupName": "Group Name",
|
||||||
|
"DescriptionFieldLabel": "Description Field"
|
||||||
|
}
|
||||||
|
});
|
10
samples/react-graph-auto-batching/src/webparts/graphAutoBatching/loc/mystrings.d.ts
vendored
Normal file
10
samples/react-graph-auto-batching/src/webparts/graphAutoBatching/loc/mystrings.d.ts
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
declare interface IGraphAutoBatchingWebPartStrings {
|
||||||
|
PropertyPaneDescription: string;
|
||||||
|
BasicGroupName: string;
|
||||||
|
DescriptionFieldLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'GraphAutoBatchingWebPartStrings' {
|
||||||
|
const strings: IGraphAutoBatchingWebPartStrings;
|
||||||
|
export = strings;
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:babb825e8471dcc8357aba5203a4100cff18002fb8cb6881975d53d062a62ebe
|
||||||
|
size 1229
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e8fc3fc81764e86c379835c47bb4cb4ddf55d135a47d36fdb6b1c9cf5da60158
|
||||||
|
size 383
|
|
@ -0,0 +1,5 @@
|
||||||
|
export class TestingUtilities {
|
||||||
|
public static sleep = (ms) => {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,749 @@
|
||||||
|
///<reference types="jest" />
|
||||||
|
import { BatchGraphClient } from "../../../src/dal/http/BatchGraphClient";
|
||||||
|
import { assert } from "chai";
|
||||||
|
import { TestingUtilities } from "../../TestingUtilities";
|
||||||
|
import { BatchHandler } from "../../../src/dal/http/BatchHandler";
|
||||||
|
|
||||||
|
describe("BatchGraphClient", ()=>{
|
||||||
|
test("should batch get requests", async () => {
|
||||||
|
let baseClient = {
|
||||||
|
get: (url, options) => {
|
||||||
|
throw "Don't call get!"
|
||||||
|
},
|
||||||
|
post: (url, options) => {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
responses: [
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
displayName: "Test User"
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/me")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
value: [{
|
||||||
|
id: "Test group id"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/me/groups")
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let batchClient = new BatchGraphClient(baseClient as any);
|
||||||
|
let mePromise = batchClient.get("/me");
|
||||||
|
let groupPromise = batchClient.get("/me/groups");
|
||||||
|
let me = await mePromise;
|
||||||
|
let group = await groupPromise;
|
||||||
|
assert.deepEqual((await me.json()), {
|
||||||
|
displayName: "Test User"
|
||||||
|
});
|
||||||
|
assert.deepEqual((await group.json()), {
|
||||||
|
value: [{
|
||||||
|
id: "Test group id"
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test("should request two batches", async () => {
|
||||||
|
let postMethod = jest.fn()
|
||||||
|
postMethod.mockReturnValueOnce(Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
responses: [
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
displayName: "Test User"
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/me")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
value: [{
|
||||||
|
id: "Test group id"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/me/groups")
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})).mockReturnValueOnce(Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
responses: [
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
displayName: "Test User second batch"
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/me")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
value: [{
|
||||||
|
id: "Test group id second batch"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/me/groups")
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
let baseClient = {
|
||||||
|
post: postMethod
|
||||||
|
};
|
||||||
|
|
||||||
|
let batchClient = new BatchGraphClient(baseClient as any, 10);
|
||||||
|
let mePromise = batchClient.get("/me");
|
||||||
|
let groupPromise = batchClient.get("/me/groups");
|
||||||
|
|
||||||
|
await TestingUtilities.sleep(11);
|
||||||
|
|
||||||
|
let secondMePromise = batchClient.get("/me");
|
||||||
|
let secondGroupPromise = batchClient.get("/me/groups");
|
||||||
|
|
||||||
|
let me = await mePromise;
|
||||||
|
let group = await groupPromise;
|
||||||
|
|
||||||
|
let secondMe = await secondMePromise;
|
||||||
|
let secondGroup = await secondGroupPromise;
|
||||||
|
|
||||||
|
assert.deepEqual((await me.json()), {
|
||||||
|
displayName: "Test User"
|
||||||
|
});
|
||||||
|
assert.deepEqual((await group.json()), {
|
||||||
|
value: [{
|
||||||
|
id: "Test group id"
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
assert.deepEqual((await secondMe.json()), {
|
||||||
|
displayName: "Test User second batch"
|
||||||
|
});
|
||||||
|
assert.deepEqual((await secondGroup.json()), {
|
||||||
|
value: [{
|
||||||
|
id: "Test group id second batch"
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test("should batch get requests", async () => {
|
||||||
|
let baseClient = {
|
||||||
|
get: (url, options) => {
|
||||||
|
throw "Don't call get!"
|
||||||
|
},
|
||||||
|
post: (url, options) => {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
responses: [
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
displayName: "Test User"
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/me")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 404,
|
||||||
|
body: {
|
||||||
|
error: {
|
||||||
|
id: "group-error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/me/groups")
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let batchClient = new BatchGraphClient(baseClient as any);
|
||||||
|
let mePromise = batchClient.get("/me");
|
||||||
|
let groupPromise = batchClient.get("/me/groups");
|
||||||
|
let me = await mePromise;
|
||||||
|
try {
|
||||||
|
await groupPromise;
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
assert.deepEqual((await err.json()), {
|
||||||
|
error: {
|
||||||
|
id: "group-error"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
assert.deepEqual((await me.json()), {
|
||||||
|
displayName: "Test User"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test("should batch v1 request separately", async () => {
|
||||||
|
let baseClient = {
|
||||||
|
get: (url, options) => {
|
||||||
|
throw "Don't call get!"
|
||||||
|
},
|
||||||
|
post: (url, options) => {
|
||||||
|
if (url.indexOf("/v1.0/") >= 0) {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
responses: [
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
displayName: "Test User v1"
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/v1.0/me")
|
||||||
|
}
|
||||||
|
],
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
responses: [
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
displayName: "Test User Beta"
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/me")
|
||||||
|
}
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let batchClient = new BatchGraphClient(baseClient as any);
|
||||||
|
let mePromise = batchClient.get("/me");
|
||||||
|
let meV1Promise = batchClient.get("/v1.0/me");
|
||||||
|
Promise.all([mePromise, meV1Promise]);
|
||||||
|
let betaMe = await (await mePromise).json();
|
||||||
|
let meV1 = await (await meV1Promise).json();
|
||||||
|
|
||||||
|
assert.equal(meV1.displayName, "Test User v1");
|
||||||
|
assert.equal(betaMe.displayName, "Test User Beta");
|
||||||
|
});
|
||||||
|
test("should batch v1 request separately (different resources)", async () => {
|
||||||
|
let baseClient = {
|
||||||
|
get: (url, options) => {
|
||||||
|
throw "Don't call get!"
|
||||||
|
},
|
||||||
|
post: (url, options) => {
|
||||||
|
if (url.indexOf("/v1.0/") >= 0) {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
responses: [
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
displayName: "Test User v1"
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/v1.0/some-other-resource")
|
||||||
|
}
|
||||||
|
],
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
responses: [
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
displayName: "Test User Beta"
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/me")
|
||||||
|
}
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let batchClient = new BatchGraphClient(baseClient as any);
|
||||||
|
let mePromise = batchClient.get("/me");
|
||||||
|
let meV1Promise = batchClient.get("/v1.0/some-other-resource");
|
||||||
|
Promise.all([mePromise, meV1Promise]);
|
||||||
|
let betaMe = await (await mePromise).json();
|
||||||
|
let meV1 = await (await meV1Promise).json();
|
||||||
|
|
||||||
|
assert.equal(meV1.displayName, "Test User v1");
|
||||||
|
assert.equal(betaMe.displayName, "Test User Beta");
|
||||||
|
});
|
||||||
|
test("should retry single request", async () => {
|
||||||
|
let baseClient = {
|
||||||
|
get: (url, options) => {
|
||||||
|
throw "Don't call get!"
|
||||||
|
},
|
||||||
|
post: (url, options) => {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jest.spyOn(baseClient, 'post').mockReturnValueOnce(Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
responses: [
|
||||||
|
{
|
||||||
|
status: 429,
|
||||||
|
body: {
|
||||||
|
displayName: "Test User"
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/me")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
value: [{
|
||||||
|
id: "Test group id"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/me/groups")
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
jest.spyOn(baseClient, 'post').mockReturnValueOnce(Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
responses: [
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
displayName: "Test User"
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/me")
|
||||||
|
}
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
let batchClient = new BatchGraphClient(baseClient as any);
|
||||||
|
|
||||||
|
let mePromise = batchClient.get("/me");
|
||||||
|
let groupPromise = batchClient.get("/me/groups");
|
||||||
|
let me = await mePromise;
|
||||||
|
let group = await groupPromise;
|
||||||
|
assert.deepEqual((await me.json()), {
|
||||||
|
displayName: "Test User"
|
||||||
|
});
|
||||||
|
assert.deepEqual((await group.json()), {
|
||||||
|
value: [{
|
||||||
|
id: "Test group id"
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test("should retry single request to v1", async () => {
|
||||||
|
let baseClient = {
|
||||||
|
get: (url, options) => {
|
||||||
|
throw "Don't call get!"
|
||||||
|
},
|
||||||
|
post: (url, options) => {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jest.spyOn(baseClient, 'post').mockReturnValueOnce(Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
responses: [
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
value: [{
|
||||||
|
id: "Test group id"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/me/groups")
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
jest.spyOn(baseClient, 'post').mockReturnValueOnce(Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
responses: [
|
||||||
|
{
|
||||||
|
status: 429,
|
||||||
|
body: {
|
||||||
|
error: "Throttle"
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/v1.0/me")
|
||||||
|
}
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
jest.spyOn(baseClient, 'post').mockReturnValueOnce(Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
responses: [
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
displayName: "Test User"
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/v1.0/me")
|
||||||
|
}
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
let batchClient = new BatchGraphClient(baseClient as any);
|
||||||
|
|
||||||
|
let mePromise = batchClient.get("/v1.0/me");
|
||||||
|
let groupPromise = batchClient.get("/me/groups");
|
||||||
|
let me = await mePromise;
|
||||||
|
let group = await groupPromise;
|
||||||
|
assert.deepEqual((await me.json()), {
|
||||||
|
displayName: "Test User"
|
||||||
|
});
|
||||||
|
assert.deepEqual((await group.json()), {
|
||||||
|
value: [{
|
||||||
|
id: "Test group id"
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test("should retry 5 times and error request", async () => {
|
||||||
|
let baseClient = {
|
||||||
|
get: (url, options) => {
|
||||||
|
throw "Don't call get!"
|
||||||
|
},
|
||||||
|
post: (url, options) => {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const throttlePromise = Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
responses: [
|
||||||
|
{
|
||||||
|
status: 429,
|
||||||
|
body: {
|
||||||
|
error: "Throttle"
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/me")
|
||||||
|
}
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
jest.spyOn(baseClient, 'post').mockReturnValueOnce(Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
responses: [
|
||||||
|
{
|
||||||
|
status: 429,
|
||||||
|
body: {
|
||||||
|
error: "Throttle"
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/me")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
value: [{
|
||||||
|
id: "Test group id"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/me/groups")
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})).mockReturnValueOnce(throttlePromise)
|
||||||
|
.mockReturnValueOnce(throttlePromise)
|
||||||
|
.mockReturnValueOnce(throttlePromise)
|
||||||
|
.mockReturnValueOnce(throttlePromise)
|
||||||
|
.mockReturnValueOnce(throttlePromise)
|
||||||
|
const batchHandlerSpy = jest.spyOn(BatchHandler.prototype, "executeBatch")
|
||||||
|
let batchClient = new BatchGraphClient(baseClient as any);
|
||||||
|
|
||||||
|
let mePromise = batchClient.get("/me");
|
||||||
|
let groupPromise = batchClient.get("/me/groups");
|
||||||
|
let me = await mePromise;
|
||||||
|
let group = await groupPromise;
|
||||||
|
|
||||||
|
expect(batchHandlerSpy).toBeCalledTimes(6);
|
||||||
|
assert.isFalse(me.ok);
|
||||||
|
assert.deepEqual((await group.json()), {
|
||||||
|
value: [{
|
||||||
|
id: "Test group id"
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
batchHandlerSpy.mockClear();
|
||||||
|
});
|
||||||
|
test("should retry 5 times and error request (next request should work)", async () => {
|
||||||
|
let baseClient = {
|
||||||
|
get: (url, options) => {
|
||||||
|
throw "Don't call get!"
|
||||||
|
},
|
||||||
|
post: (url, options) => {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const throttlePromise = Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
responses: [
|
||||||
|
{
|
||||||
|
status: 429,
|
||||||
|
body: {
|
||||||
|
error: "Throttle"
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/me")
|
||||||
|
}
|
||||||
|
],
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.spyOn(baseClient, 'post').mockReturnValueOnce(Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
responses: [
|
||||||
|
{
|
||||||
|
status: 429,
|
||||||
|
body: {
|
||||||
|
error: "Throttle"
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/me")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
value: [{
|
||||||
|
id: "Test group id"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/me/groups")
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})).mockReturnValueOnce(throttlePromise)
|
||||||
|
.mockReturnValueOnce(throttlePromise)
|
||||||
|
.mockReturnValueOnce(throttlePromise)
|
||||||
|
.mockReturnValueOnce(throttlePromise)
|
||||||
|
.mockReturnValueOnce(throttlePromise)
|
||||||
|
.mockReturnValueOnce(Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
responses: [
|
||||||
|
{
|
||||||
|
body: {
|
||||||
|
displayName: "Test User"
|
||||||
|
},
|
||||||
|
status: 200,
|
||||||
|
id: encodeURIComponent("/me")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
value: [{
|
||||||
|
id: "Test group id"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/me/groups")
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
const batchHandlerSpy = jest.spyOn(BatchHandler.prototype, "executeBatch")
|
||||||
|
|
||||||
|
let batchClient = new BatchGraphClient(baseClient as any);
|
||||||
|
|
||||||
|
let mePromise = batchClient.get("/me");
|
||||||
|
let groupPromise = batchClient.get("/me/groups");
|
||||||
|
let me = await mePromise;
|
||||||
|
let group = await groupPromise;
|
||||||
|
|
||||||
|
expect(batchHandlerSpy).toBeCalledTimes(6);
|
||||||
|
assert.isFalse(me.ok);
|
||||||
|
assert.deepEqual((await group.json()), {
|
||||||
|
value: [{
|
||||||
|
id: "Test group id"
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
mePromise = batchClient.get("/me");
|
||||||
|
groupPromise = batchClient.get("/me/groups");
|
||||||
|
me = await mePromise;
|
||||||
|
group = await groupPromise;
|
||||||
|
assert.isTrue(me.ok);
|
||||||
|
|
||||||
|
assert.deepEqual((await me.json()), {
|
||||||
|
displayName: "Test User"
|
||||||
|
});
|
||||||
|
assert.deepEqual((await group.json()), {
|
||||||
|
value: [{
|
||||||
|
id: "Test group id"
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test("should retry and concurrent request should be called", async () => {
|
||||||
|
let baseClient = {
|
||||||
|
get: (url, options) => {
|
||||||
|
throw "Don't call get!"
|
||||||
|
},
|
||||||
|
post: (url, options) => {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const throttlePromise = Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
responses: [
|
||||||
|
{
|
||||||
|
status: 429,
|
||||||
|
body: {
|
||||||
|
error: "Throttle"
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/me")
|
||||||
|
}
|
||||||
|
],
|
||||||
|
})
|
||||||
|
});
|
||||||
|
//So...the first request should be throttled
|
||||||
|
jest.spyOn(baseClient, 'post').mockReturnValueOnce(throttlePromise);
|
||||||
|
//The first retry will occur before we call concurrent request, to make sure retry batch will not be overridden
|
||||||
|
jest.spyOn(baseClient, 'post').mockReturnValueOnce(Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
responses: [
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
displayName: "Test User"
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/me")
|
||||||
|
}
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
//The concurrent request should be resolved with this response
|
||||||
|
jest.spyOn(baseClient, 'post').mockReturnValueOnce(Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
responses: [
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
displayName: "Test User (concurrent)"
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/me-concurrent")
|
||||||
|
}
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
let batchClient = new BatchGraphClient(baseClient as any, 10);
|
||||||
|
//request throttled endpoint
|
||||||
|
let throttledPromise = batchClient.get("/me");
|
||||||
|
//wait 12 milliseconds to be sure retry batch is constructed
|
||||||
|
await TestingUtilities.sleep(12);
|
||||||
|
//then request another
|
||||||
|
let concurrentRequest = batchClient.get("/me-concurrent");
|
||||||
|
|
||||||
|
let meResponse = await throttledPromise;
|
||||||
|
let concurrentMeResponse = await concurrentRequest;
|
||||||
|
|
||||||
|
assert.equal((await meResponse.json()).displayName, "Test User");
|
||||||
|
assert.equal((await concurrentMeResponse.json()).displayName, "Test User (concurrent)");
|
||||||
|
});
|
||||||
|
test("should split bigger batch to subsequent requests", async () => {
|
||||||
|
let baseClient = {
|
||||||
|
get: (url, options) => {
|
||||||
|
throw "Don't call get!"
|
||||||
|
},
|
||||||
|
post: (url, options) => {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
responses: [
|
||||||
|
],
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jest.spyOn(baseClient, "post").mockReturnValueOnce(Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
responses: [
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
displayName: "Test User"
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/me")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
value: [{
|
||||||
|
id: "Test group id"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/me/groups")
|
||||||
|
}
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
jest.spyOn(baseClient, "post").mockReturnValueOnce(Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
responses: [
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
displayName: "Test User 2"
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/me2")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
body: {
|
||||||
|
value: [{
|
||||||
|
id: "Test group id 2"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
id: encodeURIComponent("/me/groups2")
|
||||||
|
}
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
let batchClient = new BatchGraphClient(baseClient as any);
|
||||||
|
batchClient.batchSplitThreshold = 2;
|
||||||
|
let mePromise = batchClient.get("/me");
|
||||||
|
let groupPromise = batchClient.get("/me/groups");
|
||||||
|
let mePromise2 = batchClient.get("/me2");
|
||||||
|
let groupPromise2 = batchClient.get("/me/groups2");
|
||||||
|
let me = await mePromise;
|
||||||
|
let group = await groupPromise;
|
||||||
|
let me2 = await mePromise2;
|
||||||
|
let group2 = await groupPromise2;
|
||||||
|
assert.deepEqual((await me.json()), {
|
||||||
|
displayName: "Test User"
|
||||||
|
});
|
||||||
|
assert.deepEqual((await group.json()), {
|
||||||
|
value: [{
|
||||||
|
id: "Test group id"
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
assert.deepEqual((await me2.json()), {
|
||||||
|
displayName: "Test User 2"
|
||||||
|
});
|
||||||
|
assert.deepEqual((await group2.json()), {
|
||||||
|
value: [{
|
||||||
|
id: "Test group id 2"
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
///<reference types="jest" />
|
||||||
|
import * as React from "react";
|
||||||
|
import { RenderResult, render, act } from "@testing-library/react";
|
||||||
|
import { assert } from "chai";
|
||||||
|
import { UserCard } from "../../../src/webparts/graphAutoBatching/components/UserCard";
|
||||||
|
|
||||||
|
describe("<UserCard />", () => {
|
||||||
|
test("should render user with presence", async ()=>{
|
||||||
|
let graphClient = {
|
||||||
|
get: async (url: string)=>Promise.resolve({})
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(graphClient, "get").mockReturnValueOnce(Promise.resolve({
|
||||||
|
status: 200,
|
||||||
|
ok: true,
|
||||||
|
json: ()=>Promise.resolve({
|
||||||
|
displayName: "Marcin Wojciechowski",
|
||||||
|
jobTitle: "Senior Software Engineer",
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
jest.spyOn(graphClient, "get").mockReturnValueOnce(Promise.resolve({
|
||||||
|
status: 200,
|
||||||
|
ok: true,
|
||||||
|
text: ()=>Promise.resolve(`"someEncodedContent"`)
|
||||||
|
}));
|
||||||
|
jest.spyOn(graphClient, "get").mockReturnValueOnce(Promise.resolve({
|
||||||
|
status: 200,
|
||||||
|
ok: true,
|
||||||
|
json: ()=>Promise.resolve({
|
||||||
|
availability: "Available"
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
let userCard: RenderResult;
|
||||||
|
await act(async ()=>{
|
||||||
|
userCard = render(<UserCard graphClient={graphClient as any} userQuery={"/me"} />);
|
||||||
|
});
|
||||||
|
const displayNameDiv = userCard.getByText("Marcin Wojciechowski");
|
||||||
|
assert.isNotNull(displayNameDiv);
|
||||||
|
|
||||||
|
const presenceDiv = userCard.getByText("Available");
|
||||||
|
assert.isNotNull(presenceDiv);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.7/includes/tsconfig-web.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"jsx": "react",
|
||||||
|
"declaration": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"outDir": "lib",
|
||||||
|
"inlineSources": false,
|
||||||
|
"strictNullChecks": false,
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"typeRoots": [
|
||||||
|
"./node_modules/@types",
|
||||||
|
"./node_modules/@microsoft"
|
||||||
|
],
|
||||||
|
"types": [
|
||||||
|
"webpack-env"
|
||||||
|
],
|
||||||
|
"lib": [
|
||||||
|
"es5",
|
||||||
|
"dom",
|
||||||
|
"es2015.collection",
|
||||||
|
"es2015.promise"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"src/**/*.tsx"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"extends": "./node_modules/@microsoft/sp-tslint-rules/base-tslint.json",
|
||||||
|
"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-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,
|
||||||
|
"variable-name": false,
|
||||||
|
"whitespace": false
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue