test(plugins): make the videos, job-proposal and wakatime specs load and run

Six service/controller specs in three plugins failed to load, so they ran
zero tests. Two defects were stacked, and the first hid the second.

1. Their testing modules load @gauzy/core, which imports uuid. The installed
   uuid (14.0.0) is ESM-only. The allow-list of ESM packages Jest must
   transform lived in packages/core/jest.config.ts, with a copy in
   integration-zapier, and not in the shared preset. Move it into
   jest.preset.js so inheriting projects get it, and let core inherit it
   (jest --showConfig resolves to the byte-identical pattern). zapier keeps
   its own copy, which replaces the preset's, and now points at the preset.
   No allowJs is needed: ts-jest >= 29.3.2 compiles node_modules .js to
   CommonJS regardless. All six specs pass with allowJs unset.

2. The specs are Nest CLI "should be defined" scaffolding whose testing
   modules never resolved the dependency graph of the class under test.
   Add stub providers for exactly what each class injects, and override
   the @UseGuards guards in the controller specs. Nest builds those in the
   module that owns the controller, but they are not constructor
   dependencies. The spec changes are additive only: no it/expect changed
   and nothing declared was removed.

Verified locally, before vs after on 1b2278d329:
- the three plugins go from 0 tests run to green (videos 3/3, 8 tests;
  job-proposal 2/2; wakatime 2/2)
- the other 31 plugins with specs are unchanged, suite for suite
- the 11 non-plugin projects that inherit the preset and have specs are
  unchanged
- core passes 179/179 suites, 2174 tests
- a mutation check (drop one repository stub) turns the spec red

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Ruslan Konviser
2026-09-26 19:53:02 +02:00
co-authored by Claude Opus 5.5
parent 1b2278d329
commit 4eec30ca27
11 changed files with 118 additions and 39 deletions
+34 -1
View File
@@ -1,4 +1,37 @@
const nxPreset = require('@nx/jest/preset').default;
// Dependencies that ship ESM-only builds Jest cannot `require`. A suite that reaches one of them
// fails to LOAD rather than failing an assertion — Jest reports that as a suite error, which reads
// like "no tests here" instead of "coverage is zero", so the tests silently stop running.
//
// This list used to live in `packages/core/jest.config.ts` (with a copy in integration-zapier), so
// the other projects that inherit this preset started without it. That is why the videos,
// job-proposal and wakatime service/controller specs — whose testing modules load `@gauzy/core` —
// failed to load. It lives here so a project gets it by inheriting the preset. Keep it to packages
// actually reached by a spec, and add whole dependency subtrees (walk for `"type": "module"`) rather
// than one package per failing run:
// sanitize-html + its parser stack -> rich-html-sanitizer / public-html-sanitizer
// uuid -> time-tracking + email-check suites, and any spec that loads
// `@gauzy/core` for real
// camelcase -> time-tracking suites
// @faker-js/faker -> reached through the entity graph (core/seeds)
// @nestjs/axios -> ships a raw `index.ts` that re-exports `./dist`
// Listing packages, rather than transforming all of node_modules, bounds the transform work. The
// `(?:.*/)?` prefix also matches nested copies and same-named directories inside other packages
// (e.g. `@smithy/uuid`); that only costs some transform time.
//
// Things this depends on that are easy to break from elsewhere:
// - ts-jest (>= 29.3.2; pinned at 29.4.6) compiles a `.js` file under `node_modules` to CommonJS
// whenever Jest hands it over, whatever `allowJs` says — so a project needs nothing else for this
// list to take effect. `allowJs` only matters for repo-local `.js` files.
// - A project that sets its own `transformIgnorePatterns` REPLACES this one rather than adding to
// it: the Angular projects (for jest-preset-angular's `.mjs` handling), `docs` (a narrower list)
// and `integration-zapier` (a copy of this list, which has to track it).
// - `@gauzy/*` is deliberately absent, and adding it would do nothing. Jest resolves workspace
// packages to their real source path (`packages/<pkg>/src/index.ts`), which has no
// `node_modules/` segment, so this pattern is never consulted for them.
const transformIgnorePatterns = [
'node_modules/(?!(?:.*/)?(sanitize-html|htmlparser2|domelementtype|domhandler|domutils|dom-serializer|entities|nanoid|parse-srcset|uuid|camelcase|@faker-js|@nestjs/axios)/)'
];
module.exports = { ...nxPreset };
module.exports = { ...nxPreset, transformIgnorePatterns };
+4 -25
View File
@@ -6,32 +6,11 @@ module.exports = {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }]
},
moduleFileExtensions: ['ts', 'js', 'html'],
// Dependencies that ship ESM-only builds Jest cannot `require`. A suite that reaches one of
// them fails to LOAD rather than failing an assertion — Jest reports that as a suite error,
// which reads like "no tests here" instead of "coverage is zero", so the tests silently stop
// running. That is how both server-side sanitization suites, every time-tracking suite and
// both email-check suites came to contribute nothing.
// `transformIgnorePatterns` — the ESM-only dependencies Jest must transform — is inherited from
// `jest.preset.js`, which documents why each package is there. It used to be defined here (and
// copied into integration-zapier), so the other projects that inherit the preset started without
// it. Do not redefine it here: a project's own key REPLACES the preset's, so a copy would drift.
//
// Keep this list to packages actually reached by a spec, and add whole dependency subtrees
// (walk for `"type": "module"`) rather than one package per failing run:
// sanitize-html + its parser stack -> rich-html-sanitizer / public-html-sanitizer
// uuid -> time-tracking + email-check suites
// camelcase -> time-tracking suites
// @faker-js/faker -> reached through the entity graph (core/seeds)
// @nestjs/axios -> ships a raw `index.ts` that re-exports `./dist`
// Listing packages explicitly, rather than transforming all of node_modules, is what keeps the
// run fast.
//
// Two things this list depends on that are easy to break from elsewhere:
// - `allowJs: true` in tsconfig.spec.json — ts-jest hands a `.js` file back UNCHANGED when
// allowJs is false, so without it every exception here silently stops working.
// - `@gauzy/*` is deliberately absent, and adding it would do nothing. Jest resolves
// workspace packages to their real source path (`packages/<pkg>/src/index.ts`), which has
// no `node_modules/` segment, so this pattern is never consulted for them. A second, older
// copy of this key used to list `@gauzy`; it was inert there too.
transformIgnorePatterns: [
'node_modules/(?!(?:.*/)?(sanitize-html|htmlparser2|domelementtype|domhandler|domutils|dom-serializer|entities|nanoid|parse-srcset|uuid|camelcase|@faker-js|@nestjs/axios)/)'
],
// Jest's own default (`/node_modules/`) plus the fresh-database migration smoke test. That one
// spec runs the whole migration chain through ts-jest — about 3-6 minutes with a warm transform
// cache, and up to half an hour on a cold one — so leaving it in `nx test core` would make every
@@ -76,7 +76,8 @@ test-tooling layer only (`packages/plugins/integration-zapier/jest.config.ts` +
`tsconfig.spec.json`): `isolatedModules: true` on the ts-jest transform (transpile per-file instead
of type-checking the whole program — the same reason `@gauzy/core`'s own bootstrap doesn't need to
satisfy a downstream consumer's stricter flags) plus the same `transformIgnorePatterns` +
`allowJs: true` `packages/core/jest.config.ts`/`tsconfig.spec.json` already needed for the ESM-only
`allowJs: true` `packages/core/jest.config.ts`/`tsconfig.spec.json` already needed (the pattern has
since moved to the root `jest.preset.js`) for the ESM-only
deps (`uuid`, ...) reached through `@gauzy/core`'s entity graph. No production code touched by this
part of the fix.
@@ -1,11 +1,18 @@
import { Test } from '@nestjs/testing';
import { MikroOrmWakatimeRepository } from './repository/mikro-orm-wakatime.repository';
import { TypeOrmWakatimeRepository } from './repository/type-orm-wakatime.repository';
import { WakatimeController } from './wakatime.controller';
import { WakatimeService } from './wakatime.service';
describe('WakatimeController', () => {
let controller: WakatimeController;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [WakatimeService],
providers: [
WakatimeService,
// `WakatimeService` is instantiated for real here, so the repositories it injects must resolve.
{ provide: TypeOrmWakatimeRepository, useValue: {} },
{ provide: MikroOrmWakatimeRepository, useValue: {} }
],
controllers: [WakatimeController]
}).compile();
controller = module.get(WakatimeController);
@@ -1,10 +1,17 @@
import { Test } from '@nestjs/testing';
import { MikroOrmWakatimeRepository } from './repository/mikro-orm-wakatime.repository';
import { TypeOrmWakatimeRepository } from './repository/type-orm-wakatime.repository';
import { WakatimeService } from './wakatime.service';
describe('WakatimeService', () => {
let service: WakatimeService;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [WakatimeService]
providers: [
WakatimeService,
// The repositories the constructor injects; being constructible is all this spec checks.
{ provide: TypeOrmWakatimeRepository, useValue: {} },
{ provide: MikroOrmWakatimeRepository, useValue: {} }
]
}).compile();
service = module.get(WakatimeService);
});
@@ -9,9 +9,10 @@ module.exports = {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }]
},
moduleFileExtensions: ['ts', 'js', 'html'],
// Mirrors packages/core/jest.config.ts's own list, for the same reason: importing `@gauzy/core`
// reaches ESM-only builds Jest cannot `require` (e.g. `uuid`, via `core/context/request-context`).
// Keep in sync with that list if it grows.
// Mirrors the list in the root `jest.preset.js` (it used to live in packages/core/jest.config.ts),
// for the same reason: importing `@gauzy/core` reaches ESM-only builds Jest cannot `require`
// (e.g. `uuid`, via `core/context/request-context`). This key REPLACES the preset's rather than
// adding to it, so keep it in sync with that list if it grows.
transformIgnorePatterns: [
'node_modules/(?!(?:.*/)?(sanitize-html|htmlparser2|domelementtype|domhandler|domutils|dom-serializer|entities|nanoid|parse-srcset|uuid|camelcase|@faker-js|@nestjs/axios)/)'
],
@@ -1,11 +1,24 @@
import { PermissionGuard, TenantPermissionGuard } from '@gauzy/core';
import { Test, TestingModule } from '@nestjs/testing';
import { EmployeeProposalTemplateController } from './employee-proposal-template.controller';
import { EmployeeProposalTemplateService } from './employee-proposal-template.service';
describe('EmployeeProposalTemplateController', () => {
let controller: EmployeeProposalTemplateController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [EmployeeProposalTemplateController]
}).compile();
controllers: [EmployeeProposalTemplateController],
// The service the controller's constructor injects. Its own construction is covered by
// `employee-proposal-template.service.spec.ts`; here it only has to resolve.
providers: [{ provide: EmployeeProposalTemplateService, useValue: {} }]
})
// `@UseGuards` guards are instantiated by the module that owns the controller, so the real ones
// would drag in the cache manager and the role-permission service. They are not constructor
// dependencies of the controller, so they are out of scope for this check.
.overrideGuard(TenantPermissionGuard)
.useValue({ canActivate: () => true })
.overrideGuard(PermissionGuard)
.useValue({ canActivate: () => true })
.compile();
controller = module.get<EmployeeProposalTemplateController>(EmployeeProposalTemplateController);
});
it('should be defined', () => {
@@ -1,10 +1,17 @@
import { Test, TestingModule } from '@nestjs/testing';
import { EmployeeProposalTemplateService } from './employee-proposal-template.service';
import { MikroOrmEmployeeProposalTemplateRepository } from './repository/mikro-orm-employee-proposal-template.repository';
import { TypeOrmEmployeeProposalTemplateRepository } from './repository/type-orm-employee-proposal-template.repository';
describe('EmployeeProposalTemplateService', () => {
let service: EmployeeProposalTemplateService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [EmployeeProposalTemplateService]
providers: [
EmployeeProposalTemplateService,
// The repositories the constructor injects; being constructible is all this spec checks.
{ provide: TypeOrmEmployeeProposalTemplateRepository, useValue: {} },
{ provide: MikroOrmEmployeeProposalTemplateRepository, useValue: {} }
]
}).compile();
service = module.get<EmployeeProposalTemplateService>(EmployeeProposalTemplateService);
});
@@ -1,6 +1,6 @@
/**
* `@gauzy/core` is mocked so this suite stays a unit test: importing it for real pulls the whole entity
* graph (and ESM-only dependencies this package's jest config does not transform).
* graph, which takes minutes to compile.
*/
jest.mock('../../services/videos.service', () => ({ VideosService: class {} }));
jest.mock('@gauzy/core', () => ({
@@ -1,4 +1,8 @@
import { EmployeeTrackedDataGuard, PermissionGuard, TenantPermissionGuard } from '@gauzy/core';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { Test, TestingModule } from '@nestjs/testing';
import { MikroOrmVideoRepository } from './repositories/mikro-orm-video.repository';
import { TypeOrmVideoRepository } from './repositories/type-orm-video.repository';
import { VideosService } from './services/videos.service';
import { VideosController } from './videos.controller';
describe('VideosController', () => {
@@ -6,8 +10,28 @@ describe('VideosController', () => {
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [VideosController],
providers: [VideosService]
}).compile();
providers: [
VideosService,
// The controller dispatches through CQRS; these are the buses its constructor injects.
{ provide: CommandBus, useValue: {} },
{ provide: QueryBus, useValue: {} },
// `VideosService` is not a dependency of the controller, which dispatches through CQRS. It is
// kept because the original spec declared it; the module still instantiates it, so its
// repositories must resolve.
{ provide: TypeOrmVideoRepository, useValue: {} },
{ provide: MikroOrmVideoRepository, useValue: {} }
]
})
// `@UseGuards` guards are instantiated by the module that owns the controller, so the real ones
// would drag in the cache manager, the role-permission service and a DataSource. They are not
// constructor dependencies of the controller, so they are out of scope for this check.
.overrideGuard(TenantPermissionGuard)
.useValue({ canActivate: () => true })
.overrideGuard(PermissionGuard)
.useValue({ canActivate: () => true })
.overrideGuard(EmployeeTrackedDataGuard)
.useValue({ canActivate: () => true })
.compile();
controller = module.get<VideosController>(VideosController);
});
it('should be defined', () => {
@@ -1,10 +1,17 @@
import { Test, TestingModule } from '@nestjs/testing';
import { MikroOrmVideoRepository } from './repositories/mikro-orm-video.repository';
import { TypeOrmVideoRepository } from './repositories/type-orm-video.repository';
import { VideosService } from './services/videos.service';
describe('VideosService', () => {
let service: VideosService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [VideosService]
providers: [
VideosService,
// The repositories the constructor injects; being constructible is all this spec checks.
{ provide: TypeOrmVideoRepository, useValue: {} },
{ provide: MikroOrmVideoRepository, useValue: {} }
]
}).compile();
service = module.get<VideosService>(VideosService);
});