Skip to content

Commit cc7feb4

Browse files
committed
test(ci): wire jest into CI with regression specs for 2026-04-22 fixes
Previously the only CI check was `npm run build`, which type-checks but never runs tests. This commit activates jest in CI via a scoped `jest.config.ci.js` so the suite passes today while the broader ESM / @ngneat/transloco migration is tackled separately. Adds regression specs for each bug hit on the customer fire-drill: * df-loading-spinner.service.spec.ts — rapid toggles inside one tick now settle to false (was stuck-on due to stale BehaviorSubject read). * case.interceptor.spec.ts — /system/event responses pass through unchanged; /api_docs exemption still works; body request transforms still convert camelCase → snake_case. * df-script-details.submit.spec.ts — submit fallback uses `||` (not `??`) so empty completeScriptName falls back to selectedRouteItem; template wiring verified for all three mat-selects; raw serviceName lookup (no re-introduction of the api_docs → apiDocs rename). Also adds "node" to tsconfig.spec.json types so specs can `readFileSync` the committed HTML/TS for contract assertions.
1 parent 84cad42 commit cc7feb4

7 files changed

Lines changed: 255 additions & 1 deletion

File tree

.github/workflows/node.js.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,9 @@ jobs:
2525
node-version: ${{ matrix.node-version }}
2626
cache: 'npm'
2727
- run: npm ci
28+
- run: npm run lint --if-present
29+
# `test:ci` runs only the specs that currently pass. The rest are
30+
# quarantined by jest.config.ci.js until the @ngneat/transloco ESM
31+
# migration is completed. New tests should be added to that config.
32+
- run: npm run test:ci
2833
- run: npm run build --if-present

jest.config.ci.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* CI Jest config.
3+
*
4+
* Extends the base jest.config.js but scopes the run to the specs that
5+
* currently pass. The rest of the suite (55 suites as of 2026-04-22)
6+
* fails because @ngneat/transloco now ships ESM-only (.mjs) and
7+
* jest-preset-angular 13 in the current setup can't resolve it without
8+
* a broader ESM migration.
9+
*
10+
* Tracking: "Jest ESM migration for transloco" — once done, this file
11+
* can be deleted and CI can call the default `npm test`.
12+
*
13+
* New tests should be added to the testMatch list below as they are
14+
* written, so they fail CI if they regress.
15+
*/
16+
const base = require('./jest.config');
17+
18+
module.exports = {
19+
...base,
20+
testMatch: [
21+
// Regression coverage for the 2026-04-22 customer fire-drill fixes.
22+
'<rootDir>/src/app/shared/services/df-loading-spinner.service.spec.ts',
23+
'<rootDir>/src/app/shared/interceptors/case.interceptor.spec.ts',
24+
'<rootDir>/src/app/adf-event-scripts/df-script-details/df-script-details.submit.spec.ts',
25+
// Existing specs that currently pass. The rest of the suite is
26+
// quarantined until the ESM/transloco migration; route.spec.ts is
27+
// a separate pre-existing assertion mismatch.
28+
'<rootDir>/src/app/shared/utilities/url.spec.ts',
29+
'<rootDir>/src/app/shared/utilities/language.spec.ts',
30+
'<rootDir>/src/app/shared/utilities/case.spec.ts',
31+
'<rootDir>/src/app/shared/utilities/file.spec.ts',
32+
'<rootDir>/src/app/shared/services/df-breakpoint.service.spec.ts',
33+
'<rootDir>/src/app/shared/services/df-theme.service.spec.ts',
34+
],
35+
};

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"build": "ng build",
88
"watch": "ng build --watch --configuration development",
99
"test": "jest --verbose",
10+
"test:ci": "jest --ci --watchAll=false --passWithNoTests --config jest.config.ci.js",
1011
"test:coverage": "jest --coverage",
1112
"test:watch": "jest --watch",
1213
"lint": "ng lint",
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* Focused regressions for the submit path in DfScriptDetailsComponent.
3+
*
4+
* These tests do not boot the full component — Angular Router + transloco +
5+
* Material make a full TestBed harness expensive. We exercise the pure
6+
* fallback and grep the committed HTML/TS to prove that the wiring which
7+
* broke a customer install on 2026-04-22 cannot silently regress.
8+
*/
9+
import { readFileSync } from 'fs';
10+
import { join } from 'path';
11+
12+
const HTML_SRC = readFileSync(
13+
join(__dirname, 'df-script-details.component.html'),
14+
'utf8'
15+
);
16+
const TS_SRC = readFileSync(
17+
join(__dirname, 'df-script-details.component.ts'),
18+
'utf8'
19+
);
20+
21+
describe('DfScriptDetailsComponent submit logic', () => {
22+
it('falls back on empty completeScriptName to selectedRouteItem', () => {
23+
// selectedServiceItemEvent() resets completeScriptName to '' (not null).
24+
// The old code used `??` which does not treat '' as missing, so
25+
// scriptItem.name became '' and the POST URL dropped the resource name
26+
// producing a 400 "No record(s) detected" from the backend.
27+
const completeScriptName = '';
28+
const selectedRouteItem = 'test_underscore._schema.get.pre_process';
29+
30+
const name = completeScriptName || selectedRouteItem;
31+
32+
expect(name).toBe('test_underscore._schema.get.pre_process');
33+
});
34+
35+
it('prefers completeScriptName when set by selectedTable/selectedRoute', () => {
36+
const completeScriptName = 'db._schema.table_name.get.pre_process';
37+
const selectedRouteItem = 'db._schema.{table_name}.get.pre_process';
38+
39+
const name = completeScriptName || selectedRouteItem;
40+
41+
expect(name).toBe('db._schema.table_name.get.pre_process');
42+
});
43+
44+
it('submit source uses `||` (not `??`) for the name fallback', () => {
45+
// Guard the exact call site — `??` would regress the empty-string bug.
46+
expect(TS_SRC).toMatch(
47+
/name:\s*this\.completeScriptName\s*\|\|\s*this\.selectedRouteItem/
48+
);
49+
expect(TS_SRC).not.toMatch(
50+
/name:\s*this\.completeScriptName\s*\?\?\s*this\.selectedRouteItem/
51+
);
52+
});
53+
});
54+
55+
describe('DfScriptDetailsComponent template wiring contract', () => {
56+
// The three mat-selects must fire their change handlers. Removing any
57+
// (selectionChange) binding was what broke saving on the customer call.
58+
it('Script Method mat-select has (selectionChange)="selectedRoute()"', () => {
59+
const methodBlock = HTML_SRC.match(
60+
/scripts\.scriptMethod[\s\S]*?<\/mat-form-field>/
61+
);
62+
expect(methodBlock).not.toBeNull();
63+
expect(methodBlock![0]).toContain('(selectionChange)="selectedRoute()"');
64+
});
65+
66+
it('Service mat-select has (selectionChange)="selectedServiceItemEvent()"', () => {
67+
const serviceBlock = HTML_SRC.match(
68+
/'service' \| transloco[\s\S]*?<\/mat-form-field>/
69+
);
70+
expect(serviceBlock).not.toBeNull();
71+
expect(serviceBlock![0]).toContain(
72+
'(selectionChange)="selectedServiceItemEvent()"'
73+
);
74+
});
75+
76+
it('Script Type mat-select has (selectionChange)="selectedEventItemEvent()"', () => {
77+
const typeBlock = HTML_SRC.match(
78+
/scripts\.scriptType[\s\S]*?<\/mat-form-field>/
79+
);
80+
expect(typeBlock).not.toBeNull();
81+
expect(typeBlock![0]).toContain(
82+
'(selectionChange)="selectedEventItemEvent()"'
83+
);
84+
});
85+
});
86+
87+
describe('DfScriptDetailsComponent service lookup key', () => {
88+
// /system/event responses are exempt from the case interceptor
89+
// (see case.interceptor.ts). The component must look up
90+
// response[serviceName] with the raw service name; the hardcoded
91+
// api_docs -> apiDocs rename must NOT return.
92+
it('does not reintroduce the api_docs → apiDocs rename', () => {
93+
expect(TS_SRC).not.toMatch(/serviceKey\s*=\s*['"]apiDocs['"]/);
94+
});
95+
96+
it('looks up events using the raw serviceName', () => {
97+
expect(TS_SRC).toMatch(/response\[serviceName\]/);
98+
});
99+
});
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { TestBed } from '@angular/core/testing';
2+
import {
3+
HttpClient,
4+
provideHttpClient,
5+
withInterceptors,
6+
} from '@angular/common/http';
7+
import {
8+
HttpTestingController,
9+
provideHttpClientTesting,
10+
} from '@angular/common/http/testing';
11+
import { caseInterceptor } from './case.interceptor';
12+
13+
describe('caseInterceptor', () => {
14+
let http: HttpClient;
15+
let httpMock: HttpTestingController;
16+
17+
beforeEach(() => {
18+
TestBed.configureTestingModule({
19+
providers: [
20+
provideHttpClient(withInterceptors([caseInterceptor])),
21+
provideHttpClientTesting(),
22+
],
23+
});
24+
http = TestBed.inject(HttpClient);
25+
httpMock = TestBed.inject(HttpTestingController);
26+
});
27+
28+
afterEach(() => httpMock.verify());
29+
30+
it('transforms snake_case response keys to camelCase for normal endpoints', done => {
31+
http.get('/api/v2/system/service').subscribe((body: any) => {
32+
expect(body.some_key).toBeUndefined();
33+
expect(body.someKey).toBe('v');
34+
done();
35+
});
36+
const req = httpMock.expectOne('/api/v2/system/service');
37+
req.flush(
38+
{ some_key: 'v' },
39+
{ headers: { 'Content-Type': 'application/json' } }
40+
);
41+
});
42+
43+
it('passes /system/event responses through unchanged', done => {
44+
// Regression: event names are response keys and the backend matches
45+
// scripts by the raw string. Transforming to camelCase would break the
46+
// Script Type dropdown and corrupt saved script names for any service
47+
// whose name contains an underscore.
48+
const raw = {
49+
test_underscore: {
50+
'test_underscore._schema': { endpoints: ['a.b.c'] },
51+
},
52+
};
53+
http
54+
.get('/api/v2/system/event?services_only=true')
55+
.subscribe((body: any) => {
56+
expect(body).toEqual(raw);
57+
done();
58+
});
59+
const req = httpMock.expectOne('/api/v2/system/event?services_only=true');
60+
req.flush(raw, { headers: { 'Content-Type': 'application/json' } });
61+
});
62+
63+
it('passes /api_docs responses through unchanged (OpenAPI keys must survive)', done => {
64+
const raw = { 'x-custom': 1, paths: { '/foo_bar': {} } };
65+
http.get('/api/v2/api_docs/swagger').subscribe((body: any) => {
66+
expect(body).toEqual(raw);
67+
done();
68+
});
69+
const req = httpMock.expectOne('/api/v2/api_docs/swagger');
70+
req.flush(raw, { headers: { 'Content-Type': 'application/json' } });
71+
});
72+
73+
it('converts camelCase request bodies to snake_case', () => {
74+
http
75+
.post('/api/v2/system/service', { isActive: true, apiKey: 'x' })
76+
.subscribe();
77+
const req = httpMock.expectOne('/api/v2/system/service');
78+
expect(req.request.body).toEqual({ is_active: true, api_key: 'x' });
79+
req.flush({});
80+
});
81+
82+
it('does not transform requests that fall outside /api', () => {
83+
http.get('/dreamfactory/dist/assets/i18n/en.json').subscribe();
84+
const req = httpMock.expectOne('/dreamfactory/dist/assets/i18n/en.json');
85+
req.flush(
86+
{ some_key: 'v' },
87+
{ headers: { 'Content-Type': 'application/json' } }
88+
);
89+
// no assertion besides: no error, no transform applied
90+
});
91+
});

src/app/shared/services/df-loading-spinner.service.spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,27 @@ describe('DfLoadingSpinnerService', () => {
4949

5050
service.active = false;
5151
});
52+
53+
it('settles to inactive when three requests start and finish inside one tick', async () => {
54+
// Regression for the stuck spinner bug: the previous setTimeout-based
55+
// implementation compared shouldBeActive to active$.value, which was
56+
// stale during rapid toggles and lost the final deactivate. After
57+
// queuedMicrotasks run, the subject must be false.
58+
const observed: boolean[] = [];
59+
service.active.subscribe(v => observed.push(v));
60+
61+
// Simulate three concurrent requests starting and finishing in one tick.
62+
service.active = true;
63+
service.active = true;
64+
service.active = true;
65+
service.active = false;
66+
service.active = false;
67+
service.active = false;
68+
69+
// Flush all pending microtasks.
70+
await Promise.resolve();
71+
await Promise.resolve();
72+
73+
expect(observed[observed.length - 1]).toBe(false);
74+
});
5275
});

tsconfig.spec.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"compilerOptions": {
55
"outDir": "./out-tsc/spec",
66
"module": "CommonJs",
7-
"types": ["jest"]
7+
"types": ["jest", "node"]
88
},
99
"include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
1010
}

0 commit comments

Comments
 (0)