@beinformed/ui 1.23.5 → 1.23.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
- ### [1.23.5](https://git.beinformed.com/public/nl.beinformed.bi.layout.lib.ui/compare/v1.23.2...v1.23.5) (2025-07-04)
5
+ ### [1.23.6](https://git.beinformed.com/public/nl.beinformed.bi.layout.lib.ui/compare/v1.23.2...v1.23.6) (2025-08-21)
6
6
 
7
7
  ### Bug Fixes
8
8
 
9
- - **server-request:** escape querystring ([ee24771](https://git.beinformed.com/public/nl.beinformed.bi.layout.lib.ui/commit/ee24771680fb0037e856037843267777b8436886))
9
+ - **server-request:** use URLSearchParam for proper encoding ([0ba1e1d](https://git.beinformed.com/public/nl.beinformed.bi.layout.lib.ui/commit/0ba1e1d62fab34bb44da16604c07c4b6c4e00c13))
10
10
 
11
11
  ### [1.23.2](https://git.beinformed.com/public/nl.beinformed.bi.layout.lib.ui/compare/v1.23.1...v1.23.2) (2022-11-16)
12
12
 
@@ -1,40 +1,34 @@
1
- import _concatInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/concat";
1
+ import _URLSearchParams from "@babel/runtime-corejs3/core-js-stable/url-search-params";
2
2
  import _mapInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/map";
3
+ import _concatInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/concat";
3
4
  import Href from "../models/href/Href";
4
5
 
5
6
  /**
6
7
  */
7
8
  const encodeQueryString = queryString => {
8
- if (!queryString) return "";
9
- const params = [];
10
- queryString.split("&").forEach(part => {
11
- const [key, value] = part.split("=");
9
+ if (!queryString) return ""; // Create a URLSearchParams object from the existing query string
10
+ // It handles all the splitting, decoding, and special characters automatically.
12
11
 
13
- if (key && value) {
14
- var _context;
12
+ const params = new _URLSearchParams(queryString); // Return the encoded query string
13
+ // toString() correctly re-encodes everything.
15
14
 
16
- params.push(_concatInstanceProperty(_context = "".concat(encodeURIComponent(key), "=")).call(_context, encodeURIComponent(value)));
17
- } else if (key) {
18
- params.push(encodeURIComponent(key));
19
- }
20
- });
21
- return params.join("&");
15
+ return params.toString();
22
16
  };
23
17
  /**
24
18
  */
25
19
 
26
20
 
27
21
  export const getFullRequestUrl = request => {
28
- var _context2;
22
+ var _context;
29
23
 
30
24
  let pathInfo = request.getPathInfo() || "/";
31
25
  let queryString = request.getQueryString();
32
- pathInfo = _mapInstanceProperty(_context2 = pathInfo.split("/")).call(_context2, segment => encodeURIComponent(segment)).join("/");
26
+ pathInfo = _mapInstanceProperty(_context = pathInfo.split("/")).call(_context, segment => encodeURIComponent(segment)).join("/");
33
27
 
34
28
  if (queryString) {
35
- var _context3;
29
+ var _context2;
36
30
 
37
- return _concatInstanceProperty(_context3 = "".concat(pathInfo, "?")).call(_context3, encodeQueryString(queryString));
31
+ return _concatInstanceProperty(_context2 = "".concat(pathInfo, "?")).call(_context2, encodeQueryString(queryString));
38
32
  }
39
33
 
40
34
  return pathInfo;
@@ -1 +1 @@
1
- {"version":3,"file":"requestInformation.js","names":["Href","encodeQueryString","queryString","params","split","forEach","part","key","value","push","encodeURIComponent","join","getFullRequestUrl","request","pathInfo","getPathInfo","getQueryString","segment","getFullRequestHref","getCookieFromRequest","cookieName","getCookieByName","getPreferredLocale","locales","languageFromCookie","acceptLanguageHeader","getHeader","availableLocaleCodes"],"sources":["../../src/react-server/requestInformation.js"],"sourcesContent":["// @flow\nimport Href from \"../models/href/Href\";\n\nimport type Locales from \"../i18n/Locales\";\n\n/**\n */\nconst encodeQueryString = (queryString: string) => {\n if (!queryString) return \"\";\n\n const params = [];\n queryString.split(\"&\").forEach((part) => {\n const [key, value] = part.split(\"=\");\n if (key && value) {\n params.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);\n } else if (key) {\n params.push(encodeURIComponent(key));\n }\n });\n return params.join(\"&\");\n};\n\n/**\n */\nexport const getFullRequestUrl = (request: HttpServletRequestJava): string => {\n let pathInfo = request.getPathInfo() || \"/\";\n let queryString = request.getQueryString();\n\n pathInfo = pathInfo\n .split(\"/\")\n .map((segment) => encodeURIComponent(segment))\n .join(\"/\");\n\n if (queryString) {\n return `${pathInfo}?${encodeQueryString(queryString)}`;\n }\n\n return pathInfo;\n};\n\n/**\n */\nexport const getFullRequestHref = (request: HttpServletRequestJava): Href =>\n new Href(getFullRequestUrl(request));\n\n/**\n */\nexport const getCookieFromRequest = (\n request: HttpServletRequestJava,\n cookieName: string\n): null | string => {\n return request.getCookieByName(cookieName);\n};\n\n/**\n */\nexport const getPreferredLocale = (\n request: HttpServletRequestJava,\n locales: Locales\n): void | string => {\n const languageFromCookie = getCookieFromRequest(request, \"locale\");\n const acceptLanguageHeader =\n languageFromCookie || request.getHeader(\"Accept-Language\");\n\n // when no accept language header or cookie present, get first locale code\n if (acceptLanguageHeader === null) {\n return locales.availableLocaleCodes[0];\n }\n\n return locales.getPreferredLocale(acceptLanguageHeader);\n};\n"],"mappings":";;AACA,OAAOA,IAAP,MAAiB,qBAAjB;;AAIA;AACA;AACA,MAAMC,iBAAiB,GAAIC,WAAD,IAAyB;EACjD,IAAI,CAACA,WAAL,EAAkB,OAAO,EAAP;EAElB,MAAMC,MAAM,GAAG,EAAf;EACAD,WAAW,CAACE,KAAZ,CAAkB,GAAlB,EAAuBC,OAAvB,CAAgCC,IAAD,IAAU;IACvC,MAAM,CAACC,GAAD,EAAMC,KAAN,IAAeF,IAAI,CAACF,KAAL,CAAW,GAAX,CAArB;;IACA,IAAIG,GAAG,IAAIC,KAAX,EAAkB;MAAA;;MAChBL,MAAM,CAACM,IAAP,8CAAeC,kBAAkB,CAACH,GAAD,CAAjC,uBAA0CG,kBAAkB,CAACF,KAAD,CAA5D;IACD,CAFD,MAEO,IAAID,GAAJ,EAAS;MACdJ,MAAM,CAACM,IAAP,CAAYC,kBAAkB,CAACH,GAAD,CAA9B;IACD;EACF,CAPD;EAQA,OAAOJ,MAAM,CAACQ,IAAP,CAAY,GAAZ,CAAP;AACD,CAbD;AAeA;AACA;;;AACA,OAAO,MAAMC,iBAAiB,GAAIC,OAAD,IAA6C;EAAA;;EAC5E,IAAIC,QAAQ,GAAGD,OAAO,CAACE,WAAR,MAAyB,GAAxC;EACA,IAAIb,WAAW,GAAGW,OAAO,CAACG,cAAR,EAAlB;EAEAF,QAAQ,GAAG,iCAAAA,QAAQ,CAChBV,KADQ,CACF,GADE,mBAEHa,OAAD,IAAaP,kBAAkB,CAACO,OAAD,CAF3B,EAGRN,IAHQ,CAGH,GAHG,CAAX;;EAKA,IAAIT,WAAJ,EAAiB;IAAA;;IACf,qDAAUY,QAAV,wBAAsBb,iBAAiB,CAACC,WAAD,CAAvC;EACD;;EAED,OAAOY,QAAP;AACD,CAdM;AAgBP;AACA;;AACA,OAAO,MAAMI,kBAAkB,GAAIL,OAAD,IAChC,IAAIb,IAAJ,CAASY,iBAAiB,CAACC,OAAD,CAA1B,CADK;AAGP;AACA;;AACA,OAAO,MAAMM,oBAAoB,GAAG,CAClCN,OADkC,EAElCO,UAFkC,KAGhB;EAClB,OAAOP,OAAO,CAACQ,eAAR,CAAwBD,UAAxB,CAAP;AACD,CALM;AAOP;AACA;;AACA,OAAO,MAAME,kBAAkB,GAAG,CAChCT,OADgC,EAEhCU,OAFgC,KAGd;EAClB,MAAMC,kBAAkB,GAAGL,oBAAoB,CAACN,OAAD,EAAU,QAAV,CAA/C;EACA,MAAMY,oBAAoB,GACxBD,kBAAkB,IAAIX,OAAO,CAACa,SAAR,CAAkB,iBAAlB,CADxB,CAFkB,CAKlB;;EACA,IAAID,oBAAoB,KAAK,IAA7B,EAAmC;IACjC,OAAOF,OAAO,CAACI,oBAAR,CAA6B,CAA7B,CAAP;EACD;;EAED,OAAOJ,OAAO,CAACD,kBAAR,CAA2BG,oBAA3B,CAAP;AACD,CAdM"}
1
+ {"version":3,"file":"requestInformation.js","names":["Href","encodeQueryString","queryString","params","toString","getFullRequestUrl","request","pathInfo","getPathInfo","getQueryString","split","segment","encodeURIComponent","join","getFullRequestHref","getCookieFromRequest","cookieName","getCookieByName","getPreferredLocale","locales","languageFromCookie","acceptLanguageHeader","getHeader","availableLocaleCodes"],"sources":["../../src/react-server/requestInformation.js"],"sourcesContent":["// @flow\nimport Href from \"../models/href/Href\";\n\nimport type Locales from \"../i18n/Locales\";\n\n/**\n */\nconst encodeQueryString = (queryString: string) => {\n if (!queryString) return \"\";\n\n // Create a URLSearchParams object from the existing query string\n // It handles all the splitting, decoding, and special characters automatically.\n const params = new URLSearchParams(queryString);\n\n // Return the encoded query string\n // toString() correctly re-encodes everything.\n return params.toString();\n};\n\n/**\n */\nexport const getFullRequestUrl = (request: HttpServletRequestJava): string => {\n let pathInfo = request.getPathInfo() || \"/\";\n let queryString = request.getQueryString();\n\n pathInfo = pathInfo\n .split(\"/\")\n .map((segment) => encodeURIComponent(segment))\n .join(\"/\");\n\n if (queryString) {\n return `${pathInfo}?${encodeQueryString(queryString)}`;\n }\n\n return pathInfo;\n};\n\n/**\n */\nexport const getFullRequestHref = (request: HttpServletRequestJava): Href =>\n new Href(getFullRequestUrl(request));\n\n/**\n */\nexport const getCookieFromRequest = (\n request: HttpServletRequestJava,\n cookieName: string\n): null | string => {\n return request.getCookieByName(cookieName);\n};\n\n/**\n */\nexport const getPreferredLocale = (\n request: HttpServletRequestJava,\n locales: Locales\n): void | string => {\n const languageFromCookie = getCookieFromRequest(request, \"locale\");\n const acceptLanguageHeader =\n languageFromCookie || request.getHeader(\"Accept-Language\");\n\n // when no accept language header or cookie present, get first locale code\n if (acceptLanguageHeader === null) {\n return locales.availableLocaleCodes[0];\n }\n\n return locales.getPreferredLocale(acceptLanguageHeader);\n};\n"],"mappings":";;;AACA,OAAOA,IAAP,MAAiB,qBAAjB;;AAIA;AACA;AACA,MAAMC,iBAAiB,GAAIC,WAAD,IAAyB;EACjD,IAAI,CAACA,WAAL,EAAkB,OAAO,EAAP,CAD+B,CAGjD;EACA;;EACA,MAAMC,MAAM,GAAG,qBAAoBD,WAApB,CAAf,CALiD,CAOjD;EACA;;EACA,OAAOC,MAAM,CAACC,QAAP,EAAP;AACD,CAVD;AAYA;AACA;;;AACA,OAAO,MAAMC,iBAAiB,GAAIC,OAAD,IAA6C;EAAA;;EAC5E,IAAIC,QAAQ,GAAGD,OAAO,CAACE,WAAR,MAAyB,GAAxC;EACA,IAAIN,WAAW,GAAGI,OAAO,CAACG,cAAR,EAAlB;EAEAF,QAAQ,GAAG,gCAAAA,QAAQ,CAChBG,KADQ,CACF,GADE,kBAEHC,OAAD,IAAaC,kBAAkB,CAACD,OAAD,CAF3B,EAGRE,IAHQ,CAGH,GAHG,CAAX;;EAKA,IAAIX,WAAJ,EAAiB;IAAA;;IACf,qDAAUK,QAAV,wBAAsBN,iBAAiB,CAACC,WAAD,CAAvC;EACD;;EAED,OAAOK,QAAP;AACD,CAdM;AAgBP;AACA;;AACA,OAAO,MAAMO,kBAAkB,GAAIR,OAAD,IAChC,IAAIN,IAAJ,CAASK,iBAAiB,CAACC,OAAD,CAA1B,CADK;AAGP;AACA;;AACA,OAAO,MAAMS,oBAAoB,GAAG,CAClCT,OADkC,EAElCU,UAFkC,KAGhB;EAClB,OAAOV,OAAO,CAACW,eAAR,CAAwBD,UAAxB,CAAP;AACD,CALM;AAOP;AACA;;AACA,OAAO,MAAME,kBAAkB,GAAG,CAChCZ,OADgC,EAEhCa,OAFgC,KAGd;EAClB,MAAMC,kBAAkB,GAAGL,oBAAoB,CAACT,OAAD,EAAU,QAAV,CAA/C;EACA,MAAMe,oBAAoB,GACxBD,kBAAkB,IAAId,OAAO,CAACgB,SAAR,CAAkB,iBAAlB,CADxB,CAFkB,CAKlB;;EACA,IAAID,oBAAoB,KAAK,IAA7B,EAAmC;IACjC,OAAOF,OAAO,CAACI,oBAAR,CAA6B,CAA7B,CAAP;EACD;;EAED,OAAOJ,OAAO,CAACD,kBAAR,CAA2BG,oBAA3B,CAAP;AACD,CAdM"}
@@ -0,0 +1,266 @@
1
+ import {
2
+ getFullRequestUrl,
3
+ getFullRequestHref,
4
+ getCookieFromRequest,
5
+ getPreferredLocale,
6
+ } from "../requestInformation"; // Adjust path as needed
7
+
8
+ // Mock the Href class
9
+ const mockHref = jest.fn((url) => ({
10
+ url: url, // For easy inspection in tests
11
+ // You can add other Href methods here if they are used by your functions
12
+ toString: () => url,
13
+ }));
14
+ jest.mock("../../models/href/Href", () => {
15
+ return jest.fn().mockImplementation((url) => {
16
+ return new mockHref(url);
17
+ });
18
+ });
19
+
20
+ // Mock the Locales class
21
+ const mockLocales = {
22
+ availableLocaleCodes: ["en-US", "es-ES", "fr-FR"],
23
+ getPreferredLocale: jest.fn((header) => {
24
+ if (header.includes("en")) return "en-US";
25
+ if (header.includes("es")) return "es-ES";
26
+ if (header.includes("fr")) return "fr-FR";
27
+ return mockLocales.availableLocaleCodes[0]; // Default if no match
28
+ }),
29
+ };
30
+ jest.mock("../../i18n/Locales", () => {
31
+ return jest.fn().mockImplementation(() => {
32
+ return mockLocales;
33
+ });
34
+ });
35
+
36
+ // Helper function to create a mock HttpServletRequestJava object
37
+ const createMockRequest = ({
38
+ pathInfo = null,
39
+ queryString = null,
40
+ cookies = [],
41
+ headers = {},
42
+ } = {}) => ({
43
+ getPathInfo: jest.fn(() => pathInfo),
44
+ getQueryString: jest.fn(() => queryString),
45
+ getCookieByName: jest.fn((name) => {
46
+ const cookie = cookies.find((c) => c.name === name);
47
+ return cookie ? cookie.value : null;
48
+ }),
49
+ getHeader: jest.fn((name) => headers[name.toLowerCase()] || null),
50
+ });
51
+
52
+ describe("requestInformation", () => {
53
+ beforeEach(() => {
54
+ jest.clearAllMocks();
55
+
56
+ // Reset mock Href to its initial mock implementation
57
+ // This is important if you test multiple instantiations or interactions
58
+ // with the Href mock.
59
+ require("../../models/href/Href").mockClear();
60
+ require("../../models/href/Href").mockImplementation((url) => {
61
+ return new mockHref(url);
62
+ });
63
+ });
64
+
65
+ describe("getFullRequestUrl", () => {
66
+ it("should return the path info when no query string is present", () => {
67
+ const request = createMockRequest({
68
+ pathInfo: "/some/path",
69
+ });
70
+ expect(getFullRequestUrl(request)).toBe("/some/path");
71
+ expect(request.getPathInfo).toHaveBeenCalledTimes(1);
72
+ expect(request.getQueryString).toHaveBeenCalledTimes(1);
73
+ });
74
+
75
+ it("should return the path info with a leading slash if pathInfo is null", () => {
76
+ const request = createMockRequest({
77
+ pathInfo: null,
78
+ });
79
+ expect(getFullRequestUrl(request)).toBe("/");
80
+ });
81
+
82
+ it("should return the path info with a leading slash if pathInfo is an empty string", () => {
83
+ const request = createMockRequest({
84
+ pathInfo: "",
85
+ });
86
+ expect(getFullRequestUrl(request)).toBe("/");
87
+ });
88
+
89
+ it("should return the path info with an encoded query string", () => {
90
+ const request = createMockRequest({
91
+ pathInfo: "/search",
92
+ queryString: "q=hello world&filter=test",
93
+ });
94
+ expect(getFullRequestUrl(request)).toBe(
95
+ "/search?q=hello+world&filter=test"
96
+ );
97
+ });
98
+
99
+ it("should handle special characters in pathInfo by encoding them", () => {
100
+ const request = createMockRequest({
101
+ pathInfo: "/path with spaces/and<script>alert(1)</script>",
102
+ });
103
+ expect(getFullRequestUrl(request)).toBe(
104
+ "/path%20with%20spaces/and%3Cscript%3Ealert(1)%3C/script%3E"
105
+ );
106
+ });
107
+
108
+ it("should handle path info with multiple segments and special characters", () => {
109
+ const request = createMockRequest({
110
+ pathInfo: "/product/category/item with special chars & numbers/id-123",
111
+ });
112
+ expect(getFullRequestUrl(request)).toBe(
113
+ "/product/category/item%20with%20special%20chars%20%26%20numbers/id-123"
114
+ );
115
+ });
116
+
117
+ it("should correctly encode both path and query string with special characters", () => {
118
+ const request = createMockRequest({
119
+ pathInfo: "/api/data",
120
+ queryString: "name=John Doe & email=john@example.com",
121
+ });
122
+ expect(getFullRequestUrl(request)).toBe(
123
+ "/api/data?name=John+Doe+&+email=john%40example.com"
124
+ );
125
+ });
126
+
127
+ it("should handle an empty query string (not null) gracefully", () => {
128
+ const request = createMockRequest({
129
+ pathInfo: "/home",
130
+ queryString: "",
131
+ });
132
+ expect(getFullRequestUrl(request)).toBe("/home");
133
+ });
134
+ });
135
+
136
+ describe("getFullRequestHref", () => {
137
+ it("should return an Href instance with the full request URL", () => {
138
+ const request = createMockRequest({
139
+ pathInfo: "/products",
140
+ queryString: "id=123",
141
+ });
142
+ const hrefInstance = getFullRequestHref(request);
143
+
144
+ // Verify that Href constructor was called with the correct URL
145
+ expect(require("../../models/href/Href")).toHaveBeenCalledTimes(1);
146
+ expect(require("../../models/href/Href")).toHaveBeenCalledWith(
147
+ "/products?id=123"
148
+ );
149
+
150
+ // Verify the returned object is an instance of the mock Href
151
+ expect(hrefInstance.url).toBe("/products?id=123");
152
+ });
153
+
154
+ it("should handle complex URLs and pass them to Href", () => {
155
+ const request = createMockRequest({
156
+ pathInfo: "/search/results",
157
+ queryString:
158
+ "q=javascript%20xss&param=value&multi=option%201%2Coption2,option3",
159
+ });
160
+ const hrefInstance = getFullRequestHref(request);
161
+ expect(require("../../models/href/Href")).toHaveBeenCalledWith(
162
+ "/search/results?q=javascript+xss&param=value&multi=option+1%2Coption2%2Coption3"
163
+ );
164
+ expect(hrefInstance.url).toBe(
165
+ "/search/results?q=javascript+xss&param=value&multi=option+1%2Coption2%2Coption3"
166
+ );
167
+ });
168
+ });
169
+
170
+ describe("getCookieFromRequest", () => {
171
+ it("should return the cookie value if found", () => {
172
+ const request = createMockRequest({
173
+ cookies: [
174
+ {
175
+ name: "session_id",
176
+ value: "abc123xyz",
177
+ },
178
+ ],
179
+ });
180
+ expect(getCookieFromRequest(request, "session_id")).toBe("abc123xyz");
181
+ expect(request.getCookieByName).toHaveBeenCalledWith("session_id");
182
+ });
183
+
184
+ it("should return null if the cookie is not found", () => {
185
+ const request = createMockRequest({
186
+ cookies: [
187
+ {
188
+ name: "other_cookie",
189
+ value: "value",
190
+ },
191
+ ],
192
+ });
193
+ expect(getCookieFromRequest(request, "non_existent_cookie")).toBeNull();
194
+ expect(request.getCookieByName).toHaveBeenCalledWith(
195
+ "non_existent_cookie"
196
+ );
197
+ });
198
+
199
+ it("should handle empty cookies array", () => {
200
+ const request = createMockRequest({
201
+ cookies: [],
202
+ });
203
+ expect(getCookieFromRequest(request, "any_cookie")).toBeNull();
204
+ });
205
+ });
206
+
207
+ describe("getPreferredLocale", () => {
208
+ it("should return locale from cookie if present", () => {
209
+ const request = createMockRequest({
210
+ cookies: [
211
+ {
212
+ name: "locale",
213
+ value: "es-ES",
214
+ },
215
+ ],
216
+ headers: {
217
+ "accept-language": "en-US,en;q=0.9",
218
+ },
219
+ });
220
+ const locales = mockLocales; // Using the mocked locales directly for convenience
221
+ expect(getPreferredLocale(request, locales)).toBe("es-ES");
222
+ expect(locales.getPreferredLocale).toHaveBeenCalledWith("es-ES"); // Because cookie took precedence
223
+ });
224
+
225
+ it("should return the first available locale code if no cookie and no Accept-Language header", () => {
226
+ const request = createMockRequest({}); // No cookies, no headers
227
+ const locales = mockLocales;
228
+ expect(getPreferredLocale(request, locales)).toBe("en-US");
229
+ expect(locales.getPreferredLocale).not.toHaveBeenCalled();
230
+ });
231
+
232
+ it("should handle null Accept-Language header correctly", () => {
233
+ const request = createMockRequest({
234
+ headers: {
235
+ "accept-language": null,
236
+ },
237
+ }); // Explicitly null header
238
+ const locales = mockLocales;
239
+ expect(getPreferredLocale(request, locales)).toBe("en-US");
240
+ expect(locales.getPreferredLocale).not.toHaveBeenCalled();
241
+ });
242
+
243
+ it("should handle empty string Accept-Language header correctly", () => {
244
+ const request = createMockRequest({
245
+ headers: {
246
+ "accept-language": "",
247
+ },
248
+ }); // Explicitly empty header
249
+ const locales = mockLocales;
250
+ expect(getPreferredLocale(request, locales)).toBe("en-US");
251
+ expect(locales.getPreferredLocale).not.toHaveBeenCalled();
252
+ });
253
+
254
+ it("should call getCookieFromRequest and getHeader", () => {
255
+ const request = createMockRequest({
256
+ headers: {
257
+ "accept-language": "de-DE",
258
+ },
259
+ });
260
+ const locales = mockLocales;
261
+ getPreferredLocale(request, locales);
262
+ expect(request.getCookieByName).toHaveBeenCalledWith("locale");
263
+ expect(request.getHeader).toHaveBeenCalledWith("Accept-Language");
264
+ });
265
+ });
266
+ });
@@ -7,45 +7,40 @@ Object.defineProperty(exports, "__esModule", {
7
7
  });
8
8
  exports.getPreferredLocale = exports.getFullRequestUrl = exports.getFullRequestHref = exports.getCookieFromRequest = void 0;
9
9
 
10
- var _concat = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/concat"));
10
+ var _urlSearchParams = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/url-search-params"));
11
11
 
12
12
  var _map = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/map"));
13
13
 
14
+ var _concat = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/concat"));
15
+
14
16
  var _Href = _interopRequireDefault(require("../models/href/Href"));
15
17
 
16
18
  /**
17
19
  */
18
20
  const encodeQueryString = queryString => {
19
- if (!queryString) return "";
20
- const params = [];
21
- queryString.split("&").forEach(part => {
22
- const [key, value] = part.split("=");
23
-
24
- if (key && value) {
25
- var _context;
26
-
27
- params.push((0, _concat.default)(_context = "".concat(encodeURIComponent(key), "=")).call(_context, encodeURIComponent(value)));
28
- } else if (key) {
29
- params.push(encodeURIComponent(key));
30
- }
31
- });
32
- return params.join("&");
21
+ if (!queryString) return ""; // Create a URLSearchParams object from the existing query string
22
+ // It handles all the splitting, decoding, and special characters automatically.
23
+
24
+ const params = new _urlSearchParams.default(queryString); // Return the encoded query string
25
+ // toString() correctly re-encodes everything.
26
+
27
+ return params.toString();
33
28
  };
34
29
  /**
35
30
  */
36
31
 
37
32
 
38
33
  const getFullRequestUrl = request => {
39
- var _context2;
34
+ var _context;
40
35
 
41
36
  let pathInfo = request.getPathInfo() || "/";
42
37
  let queryString = request.getQueryString();
43
- pathInfo = (0, _map.default)(_context2 = pathInfo.split("/")).call(_context2, segment => encodeURIComponent(segment)).join("/");
38
+ pathInfo = (0, _map.default)(_context = pathInfo.split("/")).call(_context, segment => encodeURIComponent(segment)).join("/");
44
39
 
45
40
  if (queryString) {
46
- var _context3;
41
+ var _context2;
47
42
 
48
- return (0, _concat.default)(_context3 = "".concat(pathInfo, "?")).call(_context3, encodeQueryString(queryString));
43
+ return (0, _concat.default)(_context2 = "".concat(pathInfo, "?")).call(_context2, encodeQueryString(queryString));
49
44
  }
50
45
 
51
46
  return pathInfo;
@@ -8,16 +8,13 @@ import type Locales from "../i18n/Locales";
8
8
  const encodeQueryString = (queryString: string) => {
9
9
  if (!queryString) return "";
10
10
 
11
- const params = [];
12
- queryString.split("&").forEach((part) => {
13
- const [key, value] = part.split("=");
14
- if (key && value) {
15
- params.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
16
- } else if (key) {
17
- params.push(encodeURIComponent(key));
18
- }
19
- });
20
- return params.join("&");
11
+ // Create a URLSearchParams object from the existing query string
12
+ // It handles all the splitting, decoding, and special characters automatically.
13
+ const params = new URLSearchParams(queryString);
14
+
15
+ // Return the encoded query string
16
+ // toString() correctly re-encodes everything.
17
+ return params.toString();
21
18
  };
22
19
 
23
20
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"requestInformation.js","names":["encodeQueryString","queryString","params","split","forEach","part","key","value","push","encodeURIComponent","join","getFullRequestUrl","request","pathInfo","getPathInfo","getQueryString","segment","getFullRequestHref","Href","getCookieFromRequest","cookieName","getCookieByName","getPreferredLocale","locales","languageFromCookie","acceptLanguageHeader","getHeader","availableLocaleCodes"],"sources":["../../src/react-server/requestInformation.js"],"sourcesContent":["// @flow\nimport Href from \"../models/href/Href\";\n\nimport type Locales from \"../i18n/Locales\";\n\n/**\n */\nconst encodeQueryString = (queryString: string) => {\n if (!queryString) return \"\";\n\n const params = [];\n queryString.split(\"&\").forEach((part) => {\n const [key, value] = part.split(\"=\");\n if (key && value) {\n params.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);\n } else if (key) {\n params.push(encodeURIComponent(key));\n }\n });\n return params.join(\"&\");\n};\n\n/**\n */\nexport const getFullRequestUrl = (request: HttpServletRequestJava): string => {\n let pathInfo = request.getPathInfo() || \"/\";\n let queryString = request.getQueryString();\n\n pathInfo = pathInfo\n .split(\"/\")\n .map((segment) => encodeURIComponent(segment))\n .join(\"/\");\n\n if (queryString) {\n return `${pathInfo}?${encodeQueryString(queryString)}`;\n }\n\n return pathInfo;\n};\n\n/**\n */\nexport const getFullRequestHref = (request: HttpServletRequestJava): Href =>\n new Href(getFullRequestUrl(request));\n\n/**\n */\nexport const getCookieFromRequest = (\n request: HttpServletRequestJava,\n cookieName: string\n): null | string => {\n return request.getCookieByName(cookieName);\n};\n\n/**\n */\nexport const getPreferredLocale = (\n request: HttpServletRequestJava,\n locales: Locales\n): void | string => {\n const languageFromCookie = getCookieFromRequest(request, \"locale\");\n const acceptLanguageHeader =\n languageFromCookie || request.getHeader(\"Accept-Language\");\n\n // when no accept language header or cookie present, get first locale code\n if (acceptLanguageHeader === null) {\n return locales.availableLocaleCodes[0];\n }\n\n return locales.getPreferredLocale(acceptLanguageHeader);\n};\n"],"mappings":";;;;;;;;;;;;;AACA;;AAIA;AACA;AACA,MAAMA,iBAAiB,GAAIC,WAAD,IAAyB;EACjD,IAAI,CAACA,WAAL,EAAkB,OAAO,EAAP;EAElB,MAAMC,MAAM,GAAG,EAAf;EACAD,WAAW,CAACE,KAAZ,CAAkB,GAAlB,EAAuBC,OAAvB,CAAgCC,IAAD,IAAU;IACvC,MAAM,CAACC,GAAD,EAAMC,KAAN,IAAeF,IAAI,CAACF,KAAL,CAAW,GAAX,CAArB;;IACA,IAAIG,GAAG,IAAIC,KAAX,EAAkB;MAAA;;MAChBL,MAAM,CAACM,IAAP,2CAAeC,kBAAkB,CAACH,GAAD,CAAjC,uBAA0CG,kBAAkB,CAACF,KAAD,CAA5D;IACD,CAFD,MAEO,IAAID,GAAJ,EAAS;MACdJ,MAAM,CAACM,IAAP,CAAYC,kBAAkB,CAACH,GAAD,CAA9B;IACD;EACF,CAPD;EAQA,OAAOJ,MAAM,CAACQ,IAAP,CAAY,GAAZ,CAAP;AACD,CAbD;AAeA;AACA;;;AACO,MAAMC,iBAAiB,GAAIC,OAAD,IAA6C;EAAA;;EAC5E,IAAIC,QAAQ,GAAGD,OAAO,CAACE,WAAR,MAAyB,GAAxC;EACA,IAAIb,WAAW,GAAGW,OAAO,CAACG,cAAR,EAAlB;EAEAF,QAAQ,GAAG,8BAAAA,QAAQ,CAChBV,KADQ,CACF,GADE,mBAEHa,OAAD,IAAaP,kBAAkB,CAACO,OAAD,CAF3B,EAGRN,IAHQ,CAGH,GAHG,CAAX;;EAKA,IAAIT,WAAJ,EAAiB;IAAA;;IACf,kDAAUY,QAAV,wBAAsBb,iBAAiB,CAACC,WAAD,CAAvC;EACD;;EAED,OAAOY,QAAP;AACD,CAdM;AAgBP;AACA;;;;;AACO,MAAMI,kBAAkB,GAAIL,OAAD,IAChC,IAAIM,aAAJ,CAASP,iBAAiB,CAACC,OAAD,CAA1B,CADK;AAGP;AACA;;;;;AACO,MAAMO,oBAAoB,GAAG,CAClCP,OADkC,EAElCQ,UAFkC,KAGhB;EAClB,OAAOR,OAAO,CAACS,eAAR,CAAwBD,UAAxB,CAAP;AACD,CALM;AAOP;AACA;;;;;AACO,MAAME,kBAAkB,GAAG,CAChCV,OADgC,EAEhCW,OAFgC,KAGd;EAClB,MAAMC,kBAAkB,GAAGL,oBAAoB,CAACP,OAAD,EAAU,QAAV,CAA/C;EACA,MAAMa,oBAAoB,GACxBD,kBAAkB,IAAIZ,OAAO,CAACc,SAAR,CAAkB,iBAAlB,CADxB,CAFkB,CAKlB;;EACA,IAAID,oBAAoB,KAAK,IAA7B,EAAmC;IACjC,OAAOF,OAAO,CAACI,oBAAR,CAA6B,CAA7B,CAAP;EACD;;EAED,OAAOJ,OAAO,CAACD,kBAAR,CAA2BG,oBAA3B,CAAP;AACD,CAdM"}
1
+ {"version":3,"file":"requestInformation.js","names":["encodeQueryString","queryString","params","toString","getFullRequestUrl","request","pathInfo","getPathInfo","getQueryString","split","segment","encodeURIComponent","join","getFullRequestHref","Href","getCookieFromRequest","cookieName","getCookieByName","getPreferredLocale","locales","languageFromCookie","acceptLanguageHeader","getHeader","availableLocaleCodes"],"sources":["../../src/react-server/requestInformation.js"],"sourcesContent":["// @flow\nimport Href from \"../models/href/Href\";\n\nimport type Locales from \"../i18n/Locales\";\n\n/**\n */\nconst encodeQueryString = (queryString: string) => {\n if (!queryString) return \"\";\n\n // Create a URLSearchParams object from the existing query string\n // It handles all the splitting, decoding, and special characters automatically.\n const params = new URLSearchParams(queryString);\n\n // Return the encoded query string\n // toString() correctly re-encodes everything.\n return params.toString();\n};\n\n/**\n */\nexport const getFullRequestUrl = (request: HttpServletRequestJava): string => {\n let pathInfo = request.getPathInfo() || \"/\";\n let queryString = request.getQueryString();\n\n pathInfo = pathInfo\n .split(\"/\")\n .map((segment) => encodeURIComponent(segment))\n .join(\"/\");\n\n if (queryString) {\n return `${pathInfo}?${encodeQueryString(queryString)}`;\n }\n\n return pathInfo;\n};\n\n/**\n */\nexport const getFullRequestHref = (request: HttpServletRequestJava): Href =>\n new Href(getFullRequestUrl(request));\n\n/**\n */\nexport const getCookieFromRequest = (\n request: HttpServletRequestJava,\n cookieName: string\n): null | string => {\n return request.getCookieByName(cookieName);\n};\n\n/**\n */\nexport const getPreferredLocale = (\n request: HttpServletRequestJava,\n locales: Locales\n): void | string => {\n const languageFromCookie = getCookieFromRequest(request, \"locale\");\n const acceptLanguageHeader =\n languageFromCookie || request.getHeader(\"Accept-Language\");\n\n // when no accept language header or cookie present, get first locale code\n if (acceptLanguageHeader === null) {\n return locales.availableLocaleCodes[0];\n }\n\n return locales.getPreferredLocale(acceptLanguageHeader);\n};\n"],"mappings":";;;;;;;;;;;;;;;AACA;;AAIA;AACA;AACA,MAAMA,iBAAiB,GAAIC,WAAD,IAAyB;EACjD,IAAI,CAACA,WAAL,EAAkB,OAAO,EAAP,CAD+B,CAGjD;EACA;;EACA,MAAMC,MAAM,GAAG,6BAAoBD,WAApB,CAAf,CALiD,CAOjD;EACA;;EACA,OAAOC,MAAM,CAACC,QAAP,EAAP;AACD,CAVD;AAYA;AACA;;;AACO,MAAMC,iBAAiB,GAAIC,OAAD,IAA6C;EAAA;;EAC5E,IAAIC,QAAQ,GAAGD,OAAO,CAACE,WAAR,MAAyB,GAAxC;EACA,IAAIN,WAAW,GAAGI,OAAO,CAACG,cAAR,EAAlB;EAEAF,QAAQ,GAAG,6BAAAA,QAAQ,CAChBG,KADQ,CACF,GADE,kBAEHC,OAAD,IAAaC,kBAAkB,CAACD,OAAD,CAF3B,EAGRE,IAHQ,CAGH,GAHG,CAAX;;EAKA,IAAIX,WAAJ,EAAiB;IAAA;;IACf,kDAAUK,QAAV,wBAAsBN,iBAAiB,CAACC,WAAD,CAAvC;EACD;;EAED,OAAOK,QAAP;AACD,CAdM;AAgBP;AACA;;;;;AACO,MAAMO,kBAAkB,GAAIR,OAAD,IAChC,IAAIS,aAAJ,CAASV,iBAAiB,CAACC,OAAD,CAA1B,CADK;AAGP;AACA;;;;;AACO,MAAMU,oBAAoB,GAAG,CAClCV,OADkC,EAElCW,UAFkC,KAGhB;EAClB,OAAOX,OAAO,CAACY,eAAR,CAAwBD,UAAxB,CAAP;AACD,CALM;AAOP;AACA;;;;;AACO,MAAME,kBAAkB,GAAG,CAChCb,OADgC,EAEhCc,OAFgC,KAGd;EAClB,MAAMC,kBAAkB,GAAGL,oBAAoB,CAACV,OAAD,EAAU,QAAV,CAA/C;EACA,MAAMgB,oBAAoB,GACxBD,kBAAkB,IAAIf,OAAO,CAACiB,SAAR,CAAkB,iBAAlB,CADxB,CAFkB,CAKlB;;EACA,IAAID,oBAAoB,KAAK,IAA7B,EAAmC;IACjC,OAAOF,OAAO,CAACI,oBAAR,CAA6B,CAA7B,CAAP;EACD;;EAED,OAAOJ,OAAO,CAACD,kBAAR,CAA2BG,oBAA3B,CAAP;AACD,CAdM"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beinformed/ui",
3
- "version": "1.23.5",
3
+ "version": "1.23.6",
4
4
  "description": "Toolbox for be informed javascript layouts",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "bugs": "http://support.beinformed.com",
@@ -33,7 +33,7 @@
33
33
  "test:ci": "jest --ci",
34
34
  "test:changed": "jest --onlyChanged",
35
35
  "upgrade-interactive": "npm-check -u",
36
- "release": "standard-version",
36
+ "release": "standard-version --release-as 1.23.6",
37
37
  "beta-release": "standard-version --prerelease beta",
38
38
  "prepublishOnly": "npm run build",
39
39
  "docs": "node ./.build/docs.js",
@@ -0,0 +1,266 @@
1
+ import {
2
+ getFullRequestUrl,
3
+ getFullRequestHref,
4
+ getCookieFromRequest,
5
+ getPreferredLocale,
6
+ } from "../requestInformation"; // Adjust path as needed
7
+
8
+ // Mock the Href class
9
+ const mockHref = jest.fn((url) => ({
10
+ url: url, // For easy inspection in tests
11
+ // You can add other Href methods here if they are used by your functions
12
+ toString: () => url,
13
+ }));
14
+ jest.mock("../../models/href/Href", () => {
15
+ return jest.fn().mockImplementation((url) => {
16
+ return new mockHref(url);
17
+ });
18
+ });
19
+
20
+ // Mock the Locales class
21
+ const mockLocales = {
22
+ availableLocaleCodes: ["en-US", "es-ES", "fr-FR"],
23
+ getPreferredLocale: jest.fn((header) => {
24
+ if (header.includes("en")) return "en-US";
25
+ if (header.includes("es")) return "es-ES";
26
+ if (header.includes("fr")) return "fr-FR";
27
+ return mockLocales.availableLocaleCodes[0]; // Default if no match
28
+ }),
29
+ };
30
+ jest.mock("../../i18n/Locales", () => {
31
+ return jest.fn().mockImplementation(() => {
32
+ return mockLocales;
33
+ });
34
+ });
35
+
36
+ // Helper function to create a mock HttpServletRequestJava object
37
+ const createMockRequest = ({
38
+ pathInfo = null,
39
+ queryString = null,
40
+ cookies = [],
41
+ headers = {},
42
+ } = {}) => ({
43
+ getPathInfo: jest.fn(() => pathInfo),
44
+ getQueryString: jest.fn(() => queryString),
45
+ getCookieByName: jest.fn((name) => {
46
+ const cookie = cookies.find((c) => c.name === name);
47
+ return cookie ? cookie.value : null;
48
+ }),
49
+ getHeader: jest.fn((name) => headers[name.toLowerCase()] || null),
50
+ });
51
+
52
+ describe("requestInformation", () => {
53
+ beforeEach(() => {
54
+ jest.clearAllMocks();
55
+
56
+ // Reset mock Href to its initial mock implementation
57
+ // This is important if you test multiple instantiations or interactions
58
+ // with the Href mock.
59
+ require("../../models/href/Href").mockClear();
60
+ require("../../models/href/Href").mockImplementation((url) => {
61
+ return new mockHref(url);
62
+ });
63
+ });
64
+
65
+ describe("getFullRequestUrl", () => {
66
+ it("should return the path info when no query string is present", () => {
67
+ const request = createMockRequest({
68
+ pathInfo: "/some/path",
69
+ });
70
+ expect(getFullRequestUrl(request)).toBe("/some/path");
71
+ expect(request.getPathInfo).toHaveBeenCalledTimes(1);
72
+ expect(request.getQueryString).toHaveBeenCalledTimes(1);
73
+ });
74
+
75
+ it("should return the path info with a leading slash if pathInfo is null", () => {
76
+ const request = createMockRequest({
77
+ pathInfo: null,
78
+ });
79
+ expect(getFullRequestUrl(request)).toBe("/");
80
+ });
81
+
82
+ it("should return the path info with a leading slash if pathInfo is an empty string", () => {
83
+ const request = createMockRequest({
84
+ pathInfo: "",
85
+ });
86
+ expect(getFullRequestUrl(request)).toBe("/");
87
+ });
88
+
89
+ it("should return the path info with an encoded query string", () => {
90
+ const request = createMockRequest({
91
+ pathInfo: "/search",
92
+ queryString: "q=hello world&filter=test",
93
+ });
94
+ expect(getFullRequestUrl(request)).toBe(
95
+ "/search?q=hello+world&filter=test"
96
+ );
97
+ });
98
+
99
+ it("should handle special characters in pathInfo by encoding them", () => {
100
+ const request = createMockRequest({
101
+ pathInfo: "/path with spaces/and<script>alert(1)</script>",
102
+ });
103
+ expect(getFullRequestUrl(request)).toBe(
104
+ "/path%20with%20spaces/and%3Cscript%3Ealert(1)%3C/script%3E"
105
+ );
106
+ });
107
+
108
+ it("should handle path info with multiple segments and special characters", () => {
109
+ const request = createMockRequest({
110
+ pathInfo: "/product/category/item with special chars & numbers/id-123",
111
+ });
112
+ expect(getFullRequestUrl(request)).toBe(
113
+ "/product/category/item%20with%20special%20chars%20%26%20numbers/id-123"
114
+ );
115
+ });
116
+
117
+ it("should correctly encode both path and query string with special characters", () => {
118
+ const request = createMockRequest({
119
+ pathInfo: "/api/data",
120
+ queryString: "name=John Doe & email=john@example.com",
121
+ });
122
+ expect(getFullRequestUrl(request)).toBe(
123
+ "/api/data?name=John+Doe+&+email=john%40example.com"
124
+ );
125
+ });
126
+
127
+ it("should handle an empty query string (not null) gracefully", () => {
128
+ const request = createMockRequest({
129
+ pathInfo: "/home",
130
+ queryString: "",
131
+ });
132
+ expect(getFullRequestUrl(request)).toBe("/home");
133
+ });
134
+ });
135
+
136
+ describe("getFullRequestHref", () => {
137
+ it("should return an Href instance with the full request URL", () => {
138
+ const request = createMockRequest({
139
+ pathInfo: "/products",
140
+ queryString: "id=123",
141
+ });
142
+ const hrefInstance = getFullRequestHref(request);
143
+
144
+ // Verify that Href constructor was called with the correct URL
145
+ expect(require("../../models/href/Href")).toHaveBeenCalledTimes(1);
146
+ expect(require("../../models/href/Href")).toHaveBeenCalledWith(
147
+ "/products?id=123"
148
+ );
149
+
150
+ // Verify the returned object is an instance of the mock Href
151
+ expect(hrefInstance.url).toBe("/products?id=123");
152
+ });
153
+
154
+ it("should handle complex URLs and pass them to Href", () => {
155
+ const request = createMockRequest({
156
+ pathInfo: "/search/results",
157
+ queryString:
158
+ "q=javascript%20xss&param=value&multi=option%201%2Coption2,option3",
159
+ });
160
+ const hrefInstance = getFullRequestHref(request);
161
+ expect(require("../../models/href/Href")).toHaveBeenCalledWith(
162
+ "/search/results?q=javascript+xss&param=value&multi=option+1%2Coption2%2Coption3"
163
+ );
164
+ expect(hrefInstance.url).toBe(
165
+ "/search/results?q=javascript+xss&param=value&multi=option+1%2Coption2%2Coption3"
166
+ );
167
+ });
168
+ });
169
+
170
+ describe("getCookieFromRequest", () => {
171
+ it("should return the cookie value if found", () => {
172
+ const request = createMockRequest({
173
+ cookies: [
174
+ {
175
+ name: "session_id",
176
+ value: "abc123xyz",
177
+ },
178
+ ],
179
+ });
180
+ expect(getCookieFromRequest(request, "session_id")).toBe("abc123xyz");
181
+ expect(request.getCookieByName).toHaveBeenCalledWith("session_id");
182
+ });
183
+
184
+ it("should return null if the cookie is not found", () => {
185
+ const request = createMockRequest({
186
+ cookies: [
187
+ {
188
+ name: "other_cookie",
189
+ value: "value",
190
+ },
191
+ ],
192
+ });
193
+ expect(getCookieFromRequest(request, "non_existent_cookie")).toBeNull();
194
+ expect(request.getCookieByName).toHaveBeenCalledWith(
195
+ "non_existent_cookie"
196
+ );
197
+ });
198
+
199
+ it("should handle empty cookies array", () => {
200
+ const request = createMockRequest({
201
+ cookies: [],
202
+ });
203
+ expect(getCookieFromRequest(request, "any_cookie")).toBeNull();
204
+ });
205
+ });
206
+
207
+ describe("getPreferredLocale", () => {
208
+ it("should return locale from cookie if present", () => {
209
+ const request = createMockRequest({
210
+ cookies: [
211
+ {
212
+ name: "locale",
213
+ value: "es-ES",
214
+ },
215
+ ],
216
+ headers: {
217
+ "accept-language": "en-US,en;q=0.9",
218
+ },
219
+ });
220
+ const locales = mockLocales; // Using the mocked locales directly for convenience
221
+ expect(getPreferredLocale(request, locales)).toBe("es-ES");
222
+ expect(locales.getPreferredLocale).toHaveBeenCalledWith("es-ES"); // Because cookie took precedence
223
+ });
224
+
225
+ it("should return the first available locale code if no cookie and no Accept-Language header", () => {
226
+ const request = createMockRequest({}); // No cookies, no headers
227
+ const locales = mockLocales;
228
+ expect(getPreferredLocale(request, locales)).toBe("en-US");
229
+ expect(locales.getPreferredLocale).not.toHaveBeenCalled();
230
+ });
231
+
232
+ it("should handle null Accept-Language header correctly", () => {
233
+ const request = createMockRequest({
234
+ headers: {
235
+ "accept-language": null,
236
+ },
237
+ }); // Explicitly null header
238
+ const locales = mockLocales;
239
+ expect(getPreferredLocale(request, locales)).toBe("en-US");
240
+ expect(locales.getPreferredLocale).not.toHaveBeenCalled();
241
+ });
242
+
243
+ it("should handle empty string Accept-Language header correctly", () => {
244
+ const request = createMockRequest({
245
+ headers: {
246
+ "accept-language": "",
247
+ },
248
+ }); // Explicitly empty header
249
+ const locales = mockLocales;
250
+ expect(getPreferredLocale(request, locales)).toBe("en-US");
251
+ expect(locales.getPreferredLocale).not.toHaveBeenCalled();
252
+ });
253
+
254
+ it("should call getCookieFromRequest and getHeader", () => {
255
+ const request = createMockRequest({
256
+ headers: {
257
+ "accept-language": "de-DE",
258
+ },
259
+ });
260
+ const locales = mockLocales;
261
+ getPreferredLocale(request, locales);
262
+ expect(request.getCookieByName).toHaveBeenCalledWith("locale");
263
+ expect(request.getHeader).toHaveBeenCalledWith("Accept-Language");
264
+ });
265
+ });
266
+ });
@@ -8,16 +8,13 @@ import type Locales from "../i18n/Locales";
8
8
  const encodeQueryString = (queryString: string) => {
9
9
  if (!queryString) return "";
10
10
 
11
- const params = [];
12
- queryString.split("&").forEach((part) => {
13
- const [key, value] = part.split("=");
14
- if (key && value) {
15
- params.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
16
- } else if (key) {
17
- params.push(encodeURIComponent(key));
18
- }
19
- });
20
- return params.join("&");
11
+ // Create a URLSearchParams object from the existing query string
12
+ // It handles all the splitting, decoding, and special characters automatically.
13
+ const params = new URLSearchParams(queryString);
14
+
15
+ // Return the encoded query string
16
+ // toString() correctly re-encodes everything.
17
+ return params.toString();
21
18
  };
22
19
 
23
20
  /**