Skip to content

Commit c7c5b12

Browse files
authored
test(angular-rsbuild): add tests for dev server config utils (#97)
* fix(angular-rsbuild): extract load esm logic for tests * test(angular-rsbuild): add setup file for memfs to integration tests * test(angular-rsbuild): add unit tests to dev server utils * test(angular-rsbuild): add integration tests to dev server utils * test(angular-rsbuild): make integration tests to unit tests
1 parent f77d82c commit c7c5b12

File tree

3 files changed

+181
-15
lines changed

3 files changed

+181
-15
lines changed

packages/angular-rsbuild/src/lib/config/dev-server-config-utils.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import type { ProxyConfig } from '@rsbuild/core';
22
import assert from 'node:assert';
3-
import { existsSync, promises as fsPromises } from 'node:fs';
3+
import { existsSync } from 'node:fs';
4+
import { readFile } from 'node:fs/promises';
45
import { extname, resolve } from 'node:path';
56
import { pathToFileURL } from 'node:url';
7+
import { loadEsmModule } from './load-esm';
68

79
export async function getProxyConfig(
810
root: string,
@@ -22,7 +24,7 @@ export async function getProxyConfig(
2224

2325
switch (extname(proxyPath)) {
2426
case '.json': {
25-
const content = await fsPromises.readFile(proxyPath, 'utf-8');
27+
const content = await readFile(proxyPath, 'utf-8');
2628

2729
const { parse, printParseErrorCode } = await import('jsonc-parser');
2830
const parseErrors: import('jsonc-parser').ParseError[] = [];
@@ -84,7 +86,7 @@ export async function getProxyConfig(
8486
return normalizeProxyConfiguration(proxyConfiguration);
8587
}
8688

87-
function getJsonErrorLineColumn(offset: number, content: string) {
89+
export function getJsonErrorLineColumn(offset: number, content: string) {
8890
if (offset === 0) {
8991
return { line: 1, column: 1 };
9092
}
@@ -105,7 +107,12 @@ function getJsonErrorLineColumn(offset: number, content: string) {
105107
return { line, column: offset - position + 1 };
106108
}
107109

108-
function normalizeProxyConfiguration(
110+
/**
111+
* Normalizes the proxy configuration to ensure a consistent format.
112+
* If the input is an array, it is returned as-is.
113+
* If the input is an object, it is transformed into an array of objects.
114+
*/
115+
export function normalizeProxyConfiguration(
109116
proxy: Record<string, object> | object[]
110117
): ProxyConfig {
111118
return Array.isArray(proxy)
@@ -116,7 +123,10 @@ function normalizeProxyConfiguration(
116123
}));
117124
}
118125

119-
function assertIsError(
126+
/**
127+
* Asserts that the value is an Error instance or an object with name and message properties.
128+
*/
129+
export function assertIsError(
120130
value: unknown
121131
): asserts value is Error & { code?: string } {
122132
const isError =
@@ -128,13 +138,3 @@ function assertIsError(
128138
'message' in value);
129139
assert(isError, 'catch clause variable is not an Error instance');
130140
}
131-
132-
let load: (<T>(modulePath: string | URL) => Promise<T>) | undefined;
133-
function loadEsmModule<T>(modulePath: string | URL): Promise<T> {
134-
load ??= new Function('modulePath', `return import(modulePath);`) as Exclude<
135-
typeof load,
136-
undefined
137-
>;
138-
139-
return load(modulePath);
140-
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { describe, expect, it } from 'vitest';
2+
import {
3+
assertIsError,
4+
getJsonErrorLineColumn,
5+
getProxyConfig,
6+
normalizeProxyConfiguration,
7+
} from './dev-server-config-utils.ts';
8+
import { MEMFS_VOLUME } from '@ng-rspack/testing-utils';
9+
import * as loadEsm from './load-esm.ts';
10+
import { vol } from 'memfs';
11+
12+
describe('getJsonErrorLineColumn', () => {
13+
it('should return line and column of the error', () => {
14+
const brokenJson = `
15+
{
16+
"test": {
17+
"id": 1a,
18+
}
19+
}`;
20+
21+
const errMsg = `Expected ',' or '}' after property value in JSON at position 38`;
22+
expect(() => JSON.parse(brokenJson)).toThrow(errMsg);
23+
24+
const match = /position (\d+)/.exec(errMsg); // Extract the position from the error message (38)
25+
const offset = match ? Number(match[1]) : 0;
26+
const { line, column } = getJsonErrorLineColumn(offset, brokenJson);
27+
expect(`Parse error at line ${line}, column ${column}`).toBe(
28+
`Parse error at line 4, column 14`
29+
);
30+
});
31+
32+
it('should return line and column 0 if offset it 0', () => {
33+
expect(getJsonErrorLineColumn(0, '')).toStrictEqual({
34+
column: 1,
35+
line: 1,
36+
});
37+
});
38+
});
39+
40+
describe('assertIsError', () => {
41+
it('does nothing for a genuine Error instance', () => {
42+
const e = new Error('oops');
43+
expect(() => assertIsError(e)).not.toThrow();
44+
});
45+
46+
it('accepts a plain object with name and message (e.g. RxJS error)', () => {
47+
const rxError = { name: 'TypeError', message: 'bad type' };
48+
expect(() => assertIsError(rxError)).not.toThrow();
49+
// After narrowing, TS knows it’s Error-like:
50+
assertIsError(rxError);
51+
expect(rxError.name).toBe('TypeError');
52+
expect(rxError.message).toBe('bad type');
53+
});
54+
55+
it('throws if value is not Error-like', () => {
56+
const badValues = [
57+
null,
58+
undefined,
59+
123,
60+
'err',
61+
{ foo: 'bar' },
62+
{ name: 'X' },
63+
{ message: 'Y' },
64+
];
65+
for (const val of badValues) {
66+
expect(() => assertIsError(val as any)).toThrow(
67+
'catch clause variable is not an Error instance'
68+
);
69+
}
70+
});
71+
});
72+
73+
describe('normalizeProxyConfiguration', () => {
74+
it('returns the same array when given an array of proxy configs', () => {
75+
const input = [
76+
{ context: ['/api'], target: 'http://localhost:3000' },
77+
{ context: ['/auth'], changeOrigin: true },
78+
];
79+
80+
expect(normalizeProxyConfiguration(input)).toBe(input);
81+
});
82+
83+
it('converts multiple-object proxy configs to an array with matching entries', () => {
84+
const input = {
85+
'/api': { target: 'http://example.com' },
86+
'/auth': { changeOrigin: true, secure: false },
87+
};
88+
expect(normalizeProxyConfiguration(input)).toEqual([
89+
{ context: ['/api'], target: 'http://example.com' },
90+
{ context: ['/auth'], changeOrigin: true, secure: false },
91+
]);
92+
});
93+
94+
it('returns an empty array when given an empty proxy object', () => {
95+
expect(normalizeProxyConfiguration({})).toEqual([]);
96+
});
97+
});
98+
99+
describe('getProxyConfig integration', () => {
100+
const root = MEMFS_VOLUME;
101+
const loadEsmSpy = vi.spyOn(loadEsm, 'loadEsmModule');
102+
103+
beforeEach(() => {
104+
loadEsmSpy.mockResolvedValue({
105+
default: [
106+
{
107+
context: ['/mjs'],
108+
foo: 'bar',
109+
},
110+
],
111+
});
112+
113+
vol.reset();
114+
vol.fromJSON(
115+
{
116+
'good.json': `{
117+
"/api": { "target": "http://example.com" },
118+
}`,
119+
'bad.json': `{ /api: target }`,
120+
'config.js': `export default { "/js": { "foo": "baz" } }`,
121+
'config.mjs': `just here to make existsSync happy`,
122+
'config.cjs': `just here to make existsSync happy`,
123+
},
124+
MEMFS_VOLUME
125+
);
126+
});
127+
128+
it('returns undefined when proxyConfig is undefined', async () => {
129+
const result = await getProxyConfig(root, undefined);
130+
expect(result).toBeUndefined();
131+
});
132+
133+
it('throws if the file does not exist', async () => {
134+
await expect(getProxyConfig(root, 'no-such-file.json')).rejects.toThrow(
135+
/Proxy configuration file .*no-such-file\.json does not exist\./
136+
);
137+
});
138+
139+
it('parses a valid JSON file and normalizes it', async () => {
140+
const cfg = await getProxyConfig(root, 'good.json');
141+
expect(cfg).toEqual([{ context: ['/api'], target: 'http://example.com' }]);
142+
});
143+
144+
it('throws with detailed errors on invalid JSON', async () => {
145+
await expect(getProxyConfig(root, 'bad.json')).rejects.toThrow(
146+
/contains parse errors:/
147+
);
148+
});
149+
150+
it('loads an ESM (.mjs) config and normalizes it', async () => {
151+
const cfg = await getProxyConfig(root, 'config.mjs');
152+
expect(cfg).toEqual([{ context: ['/mjs'], foo: 'bar' }]);
153+
});
154+
155+
it.todo('loads a CommonJS (.cjs) config and normalizes it');
156+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
let load: (<T>(modulePath: string | URL) => Promise<T>) | undefined;
2+
3+
export function loadEsmModule<T>(modulePath: string | URL): Promise<T> {
4+
load ??= new Function('modulePath', `return import(modulePath);`) as Exclude<
5+
typeof load,
6+
undefined
7+
>;
8+
9+
return load(modulePath);
10+
}

0 commit comments

Comments
 (0)