Skip to content

Commit

Permalink
Merge pull request #2924 from patrick-rodgers/version-4
Browse files Browse the repository at this point in the history
Version 4
  • Loading branch information
patrick-rodgers authored Jan 30, 2024
2 parents 0fb20ab + 332a856 commit 265146e
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 1 deletion.
2 changes: 1 addition & 1 deletion packages/graph/batching.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
39 changes: 39 additions & 0 deletions packages/graph/taxonomy/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,40 @@ export class _TermSet extends _GraphInstance<ITermStoreType.Set> {
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<IOrderedTermInfo[]> {

const visitor = async (source: { children(): Promise<ITermStoreType.Term[]> }, parent: IOrderedTermInfo[]) => {

const children = await source.children();

for (let i = 0; i < children.length; i++) {

const child = children[i];

const orderedTerm: Partial<IOrderedTermInfo> = {
children: <IOrderedTermInfo[]>[],
defaultLabel: child.labels.find(l => l.isDefault).name,
...child,
};

parent.push(<any>orderedTerm);

await visitor(this.getTermById(child.id), <any>orderedTerm.children);
}
};

const tree: IOrderedTermInfo[] = [];

await visitor(this, tree);

return tree;
}
}
export interface ITermSet extends _TermSet, IUpdateable<ITermStoreType.Set>, IDeleteable { }
export const TermSet = graphInvokableFactory<ITermSet>(_TermSet);
Expand Down Expand Up @@ -122,3 +156,8 @@ export const Terms = graphInvokableFactory<ITerms>(_Terms);
export class _Relations extends _GraphCollection<ITermStoreType.Relation[]> { }
export interface IRelations extends _Relations, IAddable<Omit<ITermStoreType.Relation, "id">> { }
export const Relations = graphInvokableFactory<IRelations>(_Relations);

export interface IOrderedTermInfo extends ITermStoreType.Term {
children: ITermStoreType.Term[];
defaultLabel: string;
}
103 changes: 103 additions & 0 deletions test/test-recording-setup.md
Original file line number Diff line number Diff line change
@@ -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


0 comments on commit 265146e

Please sign in to comment.