Skip to content

Commit 4c7eb3b

Browse files
authored
Merge pull request #381 from contentstack/development
DX | 29-07-2026 | Release
2 parents 6fcb900 + 62ad828 commit 4c7eb3b

27 files changed

Lines changed: 1208 additions & 81 deletions

.talismanrc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,12 @@ fileignoreconfig:
6666
checksum: 9e7a4696561b790cb93f3be8406a70ec6fdc90a3f8bbb9739504495690158fe3
6767
- filename: src/query/term-query.ts
6868
checksum: 1f5b23177460d562076d93cf28b375106b19123a5ab135ffef75f4b2bb332d35
69+
- filename: test/bundlers/run-with-report.sh
70+
checksum: fedb0c262e3d88ad3537943e828d8ed9412a9f7d78b6406997b3955b29816f20
71+
- filename: test/utils/assertion-tracker.ts
72+
checksum: f02ce0af5948cd813020367c21da2cd0cd00168eeeb9e8af1858b852ae83e269
73+
- filename: test/utils/request-capture-plugin.ts
74+
checksum: 596fbbbf4aace2431dc165208a81f1a03c5f1d5268aceda83385debeaba79b97
75+
- filename: test/reporting/rich-html-reporter.cjs
76+
checksum: 1da275d7d083cc671a3888b1a045a616f79ac1fe023ee64ea34f0f23ddbc3706
6977
version: "1.0"

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
### Version: 5.5.0
2+
#### Date: Jul-27-2026
3+
Enhancement: Entry variants support an optional branch name as the second argument to `variants()` on `Entry` and `Entries`. When provided, the branch is sent as the `branch` request header together with `x-cs-variant-uid`. Existing `variants(uid)` and `variants(uids)` calls remain backward compatible. Added unit and API tests for variant + branch requests.
4+
15
### Version: 5.4.0
26
#### Date: Jul-16-2026
37
Enhancement: Removed `locale?` parameter from `Taxonomy.fetch()` and `Term.fetch()` — locale is now set via the chainable `.param('locale', value)` API, consistent with other query modifiers.

jest.config.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,12 @@ export default {
3838
includeConsoleLog: true,
3939
},
4040
],
41+
// Rich single-file HTML report with inline per-test HTTP context (cURL,
42+
// SDK method, request/response). Fixed path (the one the GoCD pipelines link to);
43+
// prints the absolute path at run end.
4144
[
42-
"jest-html-reporters",
43-
{
44-
publicPath: "./reports/contentstack-delivery/html",
45-
filename: "index.html",
46-
expand: true,
47-
// Enable console log capture in reports
48-
enableMergeData: true,
49-
dataMergeLevel: 2,
50-
},
45+
"./test/reporting/rich-html-reporter.cjs",
46+
{ outputPath: "reports/contentstack-delivery/html/index.html" },
5147
],
5248
[
5349
"jest-junit",

jest.setup.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@
66
*/
77
import * as fs from 'fs';
88
import * as path from 'path';
9+
import {
10+
getLastCapturedRequest,
11+
clearCapturedRequests,
12+
} from './test/utils/request-capture-plugin';
13+
import {
14+
installAssertionTracker,
15+
clearAssertions,
16+
getAssertions,
17+
} from './test/utils/assertion-tracker';
918

1019
// Store captured console logs
1120
interface ConsoleLog {
@@ -37,7 +46,7 @@ const originalConsole = {
3746
const expectedErrors = [
3847
'Invalid key:', // From query.search() validation
3948
'Invalid value (expected string or number):', // From query.equalTo() validation
40-
'Argument should be a String or an Array.', // From entry/entries.includeReference() validation
49+
'Invalid argument. Provide a string or an array', // From entry/entries.includeReference() validation (ErrorMessages.INVALID_ARGUMENT_STRING_OR_ARRAY)
4150
'Invalid fieldUid:', // From asset query validation
4251
];
4352

@@ -76,6 +85,47 @@ console.error = captureConsole('error');
7685
console.info = captureConsole('info');
7786
console.debug = captureConsole('debug');
7887

88+
// ---------------------------------------------------------------------------
89+
// Rich per-test HTTP context (cURL / SDK method / request+response / status).
90+
// Active only when ENABLE_HTTP_CAPTURE=true (the request-capture plugin is
91+
// attached to the stack instance under the same flag). Each test's last
92+
// captured HTTP call is appended to a JSONL sidecar that the custom
93+
// rich-html-reporter reads at run-end to build the single-file HTML report.
94+
// ---------------------------------------------------------------------------
95+
const HTTP_CAPTURE_ENABLED = process.env.ENABLE_HTTP_CAPTURE === 'true';
96+
const CAPTURES_FILE = path.resolve(__dirname, 'test-results', 'http-captures.jsonl');
97+
98+
if (HTTP_CAPTURE_ENABLED) {
99+
beforeEach(() => {
100+
// Install inside beforeEach so it runs AFTER the spec's `import { expect } from
101+
// '@jest/globals'` has resolved the shared module object (idempotent via a guard).
102+
// Records every assertion (expected/actual/pass) without changing any test.
103+
installAssertionTracker();
104+
clearCapturedRequests();
105+
clearAssertions();
106+
});
107+
108+
afterEach(() => {
109+
try {
110+
const cap = getLastCapturedRequest();
111+
const assertions = getAssertions();
112+
if (!cap && assertions.length === 0) return;
113+
const state: any = (expect as any).getState();
114+
const rec = {
115+
testPath: state.testPath,
116+
testName: state.currentTestName,
117+
capture: cap || null,
118+
assertions,
119+
};
120+
const dir = path.dirname(CAPTURES_FILE);
121+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
122+
fs.appendFileSync(CAPTURES_FILE, JSON.stringify(rec) + '\n');
123+
} catch {
124+
// never let reporting break a test
125+
}
126+
});
127+
}
128+
79129
// After all tests complete, write logs to file
80130
afterAll(() => {
81131
const logsPath = path.resolve(__dirname, 'test-results', 'console-logs.json');

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@contentstack/delivery-sdk",
3-
"version": "5.4.0",
3+
"version": "5.5.0",
44
"type": "module",
55
"license": "MIT",
66
"engines": {
@@ -26,11 +26,11 @@
2626
"prepare": "npm run build",
2727
"test": "jest ./test/unit",
2828
"test:unit": "jest ./test/unit",
29-
"test:api": "jest ./test/api",
29+
"test:api": "ENABLE_HTTP_CAPTURE=true jest ./test/api",
3030
"test:browser": "jest --config jest.config.browser.ts",
3131
"test:e2e": "node test/e2e/build-browser-bundle.js && playwright test",
3232
"test:e2e:ui": "node test/e2e/build-browser-bundle.js && playwright test --ui",
33-
"test:api:report": "jest ./test/api --json --outputFile=test-results/jest-results.json",
33+
"test:api:report": "ENABLE_HTTP_CAPTURE=true jest ./test/api --json --outputFile=test-results/jest-results.json",
3434
"test:bundlers:report": "cd test/bundlers && ./run-with-report.sh",
3535
"test:cicd": "mkdir -p test-results && npm run test:api:report && npm run test:bundlers:report && npm run test:e2e && node test/reporting/generate-unified-report.js",
3636
"test:cicd:no-browser": "mkdir -p test-results && npm run test:api:report && npm run test:bundlers:report && node test/reporting/generate-unified-report.js",

src/common/utils.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,24 @@ export function encodeQueryParams(params: params): params {
3737

3838
return encodedParams;
3939
}
40+
41+
/**
42+
* Builds request headers for entry variant requests.
43+
* @param variants - Comma-separated variant UID(s)
44+
* @param branch - Optional branch name to scope the variant request
45+
*/
46+
export function buildVariantRequestHeaders(
47+
variants: string,
48+
branch?: string
49+
): Record<string, string> | undefined {
50+
const headers: Record<string, string> = {};
51+
52+
if (variants) {
53+
headers['x-cs-variant-uid'] = variants;
54+
}
55+
if (branch) {
56+
headers.branch = branch;
57+
}
58+
59+
return Object.keys(headers).length > 0 ? headers : undefined;
60+
}

src/entries/entries.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { AxiosInstance, getData } from '@contentstack/core';
22
import { Query } from '../query';
33
import { BaseQuery } from '../query';
44
import { FindResponse } from '../common/types';
5-
import { encodeQueryParams } from '../common/utils';
5+
import { buildVariantRequestHeaders, encodeQueryParams } from '../common/utils';
66
import { ErrorMessages } from '../common/error-messages';
77

88
export class Entries extends BaseQuery {
@@ -14,6 +14,7 @@ export class Entries extends BaseQuery {
1414
this._contentTypeUid = contentTypeUid;
1515
this._urlPath = `/content_types/${this._contentTypeUid}/entries`;
1616
this._variants = '';
17+
this._variantsBranch = '';
1718
}
1819

1920
/**
@@ -252,28 +253,36 @@ export class Entries extends BaseQuery {
252253
* const query = stack.contentType("contentTypeUid").entry().query();
253254
*/
254255
query(queryObj?: { [key: string]: any }) {
255-
if (queryObj) return new Query(this._client, this._parameters, this._queryParams, this._variants, this._contentTypeUid, queryObj);
256+
if (queryObj) {
257+
return new Query(this._client, this._parameters, this._queryParams, this._variants, this._contentTypeUid, this._variantsBranch, queryObj);
258+
}
256259

257-
return new Query(this._client, this._parameters, this._queryParams, this._variants, this._contentTypeUid);
260+
return new Query(this._client, this._parameters, this._queryParams, this._variants, this._contentTypeUid, this._variantsBranch);
258261
}
259262

260263
/**
261264
* @method variants
262265
* @memberof Entries
263-
* @description The variant header will be added to axios client
266+
* @description Stores the variant UID(s) and optional branch name, which are sent as the `x-cs-variant-uid` and `branch` headers on the request when find() is called.
267+
* @param {string | string[]} variants - Variant UID or UIDs
268+
* @param {string} [branchName] - Optional branch name sent as the `branch` header
264269
* @returns {Entries}
265270
* @example
266271
* import contentstack from '@contentstack/delivery-sdk'
267272
*
268273
* const stack = contentstack.stack({ apiKey: "apiKey", deliveryToken: "deliveryToken", environment: "environment" });
269274
* const result = await stack.contentType('abc').entry().variants('xyz').find();
275+
* const resultWithBranch = await stack.contentType('abc').entry().variants('xyz', 'branch_name').find();
270276
*/
271-
variants(variants: string | string[]): Entries {
277+
variants(variants: string | string[], branchName?: string): Entries {
272278
if (Array.isArray(variants) && variants.length > 0) {
273279
this._variants = variants.join(',');
274280
} else if (typeof variants == 'string' && variants.length > 0) {
275281
this._variants = variants;
276282
}
283+
if (typeof branchName === 'string' && branchName.length > 0) {
284+
this._variantsBranch = branchName;
285+
}
277286
return this;
278287
}
279288

@@ -320,10 +329,11 @@ export class Entries extends BaseQuery {
320329
contentTypeUid: this._contentTypeUid
321330
};
322331

323-
if (this._variants) {
332+
const variantHeaders = buildVariantRequestHeaders(this._variants, this._variantsBranch);
333+
if (variantHeaders) {
324334
getRequestOptions.headers = {
325335
...getRequestOptions.headers,
326-
'x-cs-variant-uid': this._variants
336+
...variantHeaders
327337
};
328338
}
329339
const response = await getData(this._client, this._urlPath, getRequestOptions);

src/entries/entry.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { AxiosInstance, getData } from '@contentstack/core';
22
import { ErrorMessages } from '../common/error-messages';
3+
import { buildVariantRequestHeaders } from '../common/utils';
34

45
interface EntryResponse<T> {
56
entry: T;
@@ -10,13 +11,15 @@ export class Entry {
1011
private _entryUid: string;
1112
private _urlPath: string;
1213
protected _variants: string;
14+
protected _variantsBranch: string;
1315
_queryParams: { [key: string]: string | number | string[] } = {};
1416
constructor(client: AxiosInstance, contentTypeUid: string, entryUid: string) {
1517
this._client = client;
1618
this._contentTypeUid = contentTypeUid;
1719
this._entryUid = entryUid;
1820
this._urlPath = `/content_types/${this._contentTypeUid}/entries/${this._entryUid}`;
1921
this._variants = '';
22+
this._variantsBranch = '';
2023
}
2124

2225
/**
@@ -39,20 +42,26 @@ export class Entry {
3942
/**
4043
* @method variants
4144
* @memberof Entry
42-
* @description The variant header will be added to axios client
45+
* @description Stores the variant UID(s) and optional branch name, which are sent as the `x-cs-variant-uid` and `branch` headers on the request when fetch() is called.
46+
* @param {string | string[]} variants - Variant UID or UIDs
47+
* @param {string} [branchName] - Optional branch name sent as the `branch` header
4348
* @returns {Entry}
4449
* @example
4550
* import contentstack from '@contentstack/delivery-sdk'
4651
*
4752
* const stack = contentstack.stack({ apiKey: "apiKey", deliveryToken: "deliveryToken", environment: "environment" });
4853
* const result = await stack.contentType('abc').entry('entry_uid').variants('xyz').fetch();
54+
* const resultWithBranch = await stack.contentType('abc').entry('entry_uid').variants('xyz', 'branch_name').fetch();
4955
*/
50-
variants(variants: string | string[]): this {
56+
variants(variants: string | string[], branchName?: string): this {
5157
if (Array.isArray(variants) && variants.length > 0) {
5258
this._variants = variants.join(',');
5359
} else if (typeof variants == 'string' && variants.length > 0) {
5460
this._variants = variants;
5561
}
62+
if (typeof branchName === 'string' && branchName.length > 0) {
63+
this._variantsBranch = branchName;
64+
}
5665

5766
return this;
5867
}
@@ -189,10 +198,11 @@ export class Entry {
189198
contentTypeUid: this._contentTypeUid,
190199
entryUid: this._entryUid
191200
};
192-
if (this._variants) {
201+
const variantHeaders = buildVariantRequestHeaders(this._variants, this._variantsBranch);
202+
if (variantHeaders) {
193203
getRequestOptions.headers = {
194204
...getRequestOptions.headers,
195-
'x-cs-variant-uid': this._variants
205+
...variantHeaders
196206
};
197207
}
198208

src/query/base-query.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { AxiosInstance, getData } from '@contentstack/core';
22
import { Pagination } from '../common/pagination';
33
import { FindResponse, params } from '../common/types';
4-
import { encodeQueryParams } from '../common/utils';
4+
import { buildVariantRequestHeaders, encodeQueryParams } from '../common/utils';
55
import type { Query } from './query';
66

77
export class BaseQuery extends Pagination {
@@ -10,6 +10,7 @@ export class BaseQuery extends Pagination {
1010
protected _client!: AxiosInstance;
1111
protected _urlPath!: string;
1212
protected _variants!: string;
13+
protected _variantsBranch!: string;
1314

1415
/**
1516
* Helper method to cast this instance to Query type
@@ -231,10 +232,11 @@ export class BaseQuery extends Pagination {
231232
contentTypeUid: this.extractContentTypeUidFromUrl()
232233
};
233234

234-
if (this._variants) {
235+
const variantHeaders = buildVariantRequestHeaders(this._variants, this._variantsBranch);
236+
if (variantHeaders) {
235237
getRequestOptions.headers = {
236238
...getRequestOptions.headers,
237-
'x-cs-variant-uid': this._variants
239+
...variantHeaders
238240
};
239241
}
240242
const response = await getData(this._client, this._urlPath, getRequestOptions);

0 commit comments

Comments
 (0)