From 1b4175971ce27088afdbb83b708ccaaa55455e16 Mon Sep 17 00:00:00 2001 From: Patrick Rodgers Date: Tue, 30 Jan 2024 09:49:30 -0500 Subject: [PATCH 1/2] update for graph taxonomy get children, test recording doc --- packages/graph/batching.ts | 2 +- packages/graph/taxonomy/types.ts | 40 ++++++++++++ test/test-recording-setup.md | 103 +++++++++++++++++++++++++++++++ 3 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 test/test-recording-setup.md diff --git a/packages/graph/batching.ts b/packages/graph/batching.ts index 71a2d2feb..aa84b7c4b 100644 --- a/packages/graph/batching.ts +++ b/packages/graph/batching.ts @@ -111,7 +111,7 @@ class BatchQueryable extends _GraphQueryable { // do a fix up on the url once other pre behaviors have had a chance to run this.on.pre(async function (this: BatchQueryable, url, init, result) { - const versRegex = /(https:\/\/.*?[\\|/]v1\.0|beta[\\|/])/i; + const versRegex = /(https:\/\/.*?\/(v1.0|beta)\/)/i; const m = url.match(versRegex); diff --git a/packages/graph/taxonomy/types.ts b/packages/graph/taxonomy/types.ts index 5ed609e09..9ce3fc7cc 100644 --- a/packages/graph/taxonomy/types.ts +++ b/packages/graph/taxonomy/types.ts @@ -1,6 +1,7 @@ import { IAddable, IDeleteable, IGetById, IUpdateable, addable, defaultPath, deleteable, getById, updateable } from "../../graph/decorators.js"; import { _GraphInstance, graphInvokableFactory, _GraphCollection } from "../graphqueryable.js"; import { TermStore as ITermStoreType } from "@microsoft/microsoft-graph-types"; +import { createBatch } from "@pnp/graph/batching"; /** * Describes a collection of Form objects @@ -75,6 +76,40 @@ export class _TermSet extends _GraphInstance { public getTermById(id: string): ITerm { return Term(this, `terms/${id}`); } + + /** + * Gets all the direct children of the current termset as a tree, however is not ordered based on the SP sorting info + * + * @returns Array of children for this item + */ + public async getAllChildrenAsTree(): Promise { + + const visitor = async (source: { children(): Promise }, parent: IOrderedTermInfo[]) => { + + const children = await source.children(); + + for (let i = 0; i < children.length; i++) { + + const child = children[i]; + + const orderedTerm: Partial = { + children: [], + defaultLabel: child.labels.find(l => l.isDefault).name, + ...child, + }; + + parent.push(orderedTerm); + + await visitor(this.getTermById(child.id), orderedTerm.children); + } + }; + + const tree: IOrderedTermInfo[] = []; + + await visitor(this, tree); + + return tree; + } } export interface ITermSet extends _TermSet, IUpdateable, IDeleteable { } export const TermSet = graphInvokableFactory(_TermSet); @@ -122,3 +157,8 @@ export const Terms = graphInvokableFactory(_Terms); export class _Relations extends _GraphCollection { } export interface IRelations extends _Relations, IAddable> { } export const Relations = graphInvokableFactory(_Relations); + +export interface IOrderedTermInfo extends ITermStoreType.Term { + children: ITermStoreType.Term[]; + defaultLabel: string; +} diff --git a/test/test-recording-setup.md b/test/test-recording-setup.md new file mode 100644 index 000000000..3b3698c5d --- /dev/null +++ b/test/test-recording-setup.md @@ -0,0 +1,103 @@ +# PnPjs Test Recording + +The testing recording is available to provide a way to record and rerun tests to save network traffic and speed up integration testing of changes, especially to core library components. + +## Activate test recording + +In testing you can use: + + `--record` flag to enable recording in read mode, which will use any recorded test data it finds + + Using `--record write` will start the recorder in write mode, meaning it will execute requests and record the results. + + ## What is recorded + + The recording records both input parameters and network responses into files stored (by default) in a `.recordings` folder. All of the properties are stored in a single file `test-props.json` in the form: + + { + "{test id guid}":{ + "name":"PnPJSTest_dTHOvBPwVN", + "id":"cf328183-0e3c-4c69-b181-fa462a958db7" + }, + "{test2 id guid}":{ + "prop1":"PnPJSTest_dTHOvBPwVN", + "prop2":"some other value" + } + // ... + } + + This allows the tests to be consistent in checking responses against input values and behave the same across runs. + + The response data is recorded in files with computed names, but starting with the test id. Some tests execute many requests and all are recorded. We record the response, request body, and request init separately as this works better with the per-request Queryable model. + + ## Adding recording to a test function + +Each test is defined by a single function, which in Mocha looks like the below. Note that on each run different random values will be used. We also have no way to identify this test against all the other tests. + +```TS +it("attachmentFiles", async function () { + + // add some attachments to an item + const r = await list.items.add({ + Title: `Test_${getRandomString(4)}`, + }); + + await r.item.attachmentFiles.add(`att_${getRandomString(4)}.txt`, "Some Content"); + await r.item.attachmentFiles.add(`att_${getRandomString(4)}.txt`, "Some Content"); + + return expect(r.item.attachmentFiles()).to.eventually.be.fulfilled.and.to.be.an("Array").and.have.length(2); +}); +``` + +To transform the test function into a PnP Test function we need to take two main steps, wrap the test function and handle the props. We wrap the test in the pnpTest wrapper function, and supply an id. This id is a new guid that must be unique within the scope of our tests. Don't worry - we throw an error if guids are reused. + +The second thing is to handle the props. To do this we augment `this` for the test with a `.props` method that takes any plain object and returns it based on some simple logic: + +|Recording Mode|Behavior| +|---|---| +|Off|Pass-through the supplied values| +|Read|Attempt to read values from the `test-props.json` data and returns the values found, or failing to find any returns the properties supplied| +|Write|Write the supplied values to `test-props.json` and return the values.| + +> Note: If you change the number or type of the properties within the test function, those recorded results will need to be updated or the test will break as the old values will be returned. There is no logic to handle cases where we stored 3 values but the test now needs 4. + + +```TS +import { pnpTest } from "../pnp-test.js"; + +it("attachmentFiles", pnpTest("9bc6dba6-6690-4453-8d13-4f42e051a245", async function () { + + const props = await this.props({ + itemTitle: `Test_${getRandomString(4)}`, + attachmentFile1Name: `att_${getRandomString(4)}.txt`, + attachmentFile2Name: `att_${getRandomString(4)}.txt`, + }); + + // add some attachments to an item + const r = await list.items.add({ + Title: props.itemTitle, + }); + + await r.item.attachmentFiles.add(props.attachmentFile1Name, "Some Content"); + await r.item.attachmentFiles.add(props.attachmentFile2Name, "Some Content"); + + return expect(r.item.attachmentFiles()).to.eventually.be.fulfilled.and.to.be.an("Array").and.have.length(2); +})); +``` + +You can use this PowerShell snippet to generate code to paste into the front of each function: + +```PowerShell +"pnpTest(""$(([guid]::NewGuid() | select Guid -expandproperty Guid | Out-String).Trim())"", " | Set-Clipboard +``` + +## How it works + +The [test recording](./test-recording.ts) replaces the default `.send` behavior with one that performs a series of steps: + +1. Generate file names for body and init +2. Look-up if files exist, and if so construct and return a new Response object based on the data +3. If no files exist and operating in read mode, make the request with node-fetch and return the Response +4. If no files exist and operating in write mode, make the request with node-fetch and write the response data to the fs + + From 332a856b7017ee85bfe67879b21ea32fe1e7518b Mon Sep 17 00:00:00 2001 From: Patrick Rodgers Date: Tue, 30 Jan 2024 09:49:52 -0500 Subject: [PATCH 2/2] update for graph taxonomy get children, test recording doc --- packages/graph/taxonomy/types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/graph/taxonomy/types.ts b/packages/graph/taxonomy/types.ts index 9ce3fc7cc..7e4858b1c 100644 --- a/packages/graph/taxonomy/types.ts +++ b/packages/graph/taxonomy/types.ts @@ -1,7 +1,6 @@ import { IAddable, IDeleteable, IGetById, IUpdateable, addable, defaultPath, deleteable, getById, updateable } from "../../graph/decorators.js"; import { _GraphInstance, graphInvokableFactory, _GraphCollection } from "../graphqueryable.js"; import { TermStore as ITermStoreType } from "@microsoft/microsoft-graph-types"; -import { createBatch } from "@pnp/graph/batching"; /** * Describes a collection of Form objects