diff --git a/Dockerfile b/Dockerfile index 90b32050..7a75b8a6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM node:20-bookworm-slim AS builder # Create app directory WORKDIR /app -COPY ./prisma/schema.prisma ./ +COPY ./prisma ./prisma # A wildcard is used to ensure both package.json AND package-lock.json are copied COPY package*.json ./ diff --git a/jest.config.ts b/jest.config.ts index 53903ecd..92d19b33 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -2,9 +2,10 @@ module.exports = async () => { return { projects: [ - './src/jest.config.ts', - './test/jest.config.ts', - './test_acceptance/jest.config.ts' + './src/jest.config.ts', + './test/jest.config.ts', + './test_acceptance/jest.config.ts', + './test_ldap/jest.config.ts' ], roots: ['./'], testTimeout: 30000, diff --git a/package-lock.json b/package-lock.json index 7a8a8c91..62e63379 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,7 @@ "passport-local": "^1.0.0", "pg": "^8.16.3", "pg-hstore": "^2.3.4", - "pixelmatch": "^5.3.0", + "pixelmatch": "^7.1.0", "pngjs": "^7.0.0", "reflect-metadata": "^0.1.13", "rimraf": "^5.0.1", @@ -61,7 +61,6 @@ "@types/node": "^20.14.10", "@types/passport-jwt": "^3.0.9", "@types/passport-local": "^1.0.38", - "@types/pixelmatch": "^5.2.6", "@types/pngjs": "^6.0.5", "@types/supertest": "^2.0.12", "@typescript-eslint/eslint-plugin": "^8.46.3", @@ -5175,16 +5174,6 @@ "@types/passport": "*" } }, - "node_modules/@types/pixelmatch": { - "version": "5.2.6", - "resolved": "https://registry.npmjs.org/@types/pixelmatch/-/pixelmatch-5.2.6.tgz", - "integrity": "sha512-wC83uexE5KGuUODn6zkm9gMzTwdY5L0chiK+VrKcDfEjzxh1uadlWTvOmAbCpnM9zx/Ww3f8uKlYQVnO/TrqVg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/pngjs": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.5.tgz", @@ -12036,24 +12025,17 @@ } }, "node_modules/pixelmatch": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.3.0.tgz", - "integrity": "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-7.1.0.tgz", + "integrity": "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==", + "license": "ISC", "dependencies": { - "pngjs": "^6.0.0" + "pngjs": "^7.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, - "node_modules/pixelmatch/node_modules/pngjs": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", - "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==", - "engines": { - "node": ">=12.13.0" - } - }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", diff --git a/package.json b/package.json index d5c743d3..89864888 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "passport-local": "^1.0.0", "pg": "^8.16.3", "pg-hstore": "^2.3.4", - "pixelmatch": "^5.3.0", + "pixelmatch": "^7.1.0", "pngjs": "^7.0.0", "reflect-metadata": "^0.1.13", "rimraf": "^5.0.1", @@ -79,7 +79,6 @@ "@types/node": "^20.14.10", "@types/passport-jwt": "^3.0.9", "@types/passport-local": "^1.0.38", - "@types/pixelmatch": "^5.2.6", "@types/pngjs": "^6.0.5", "@types/supertest": "^2.0.12", "@typescript-eslint/eslint-plugin": "^8.46.3", diff --git a/prisma/Dockerfile b/prisma/Dockerfile index f9ca9969..df2015f5 100644 --- a/prisma/Dockerfile +++ b/prisma/Dockerfile @@ -2,16 +2,17 @@ FROM node:20-bookworm-slim ENV DEBIAN_FRONTEND=noninteractive RUN apt update && apt install -y libssl3 && rm -rf /var/lib/apt/lists/* -WORKDIR /app +WORKDIR /workspace -COPY . . +COPY . ./prisma -ENV NODE_TLS_REJECT_UNAUTHORIZED='0' -RUN npm ci --verbose +WORKDIR /workspace/prisma -RUN chmod +x /app/wait-for-it.sh -RUN chmod +x /app/entrypoint.sh +ENV NODE_TLS_REJECT_UNAUTHORIZED='0' +RUN npm ci --verbose && \ + chmod +x /workspace/prisma/wait-for-it.sh && \ + chmod +x /workspace/prisma/entrypoint.sh -ENTRYPOINT ["/app/entrypoint.sh"] +ENTRYPOINT ["/workspace/prisma/entrypoint.sh"] CMD ["sh"] \ No newline at end of file diff --git a/prisma/entrypoint.sh b/prisma/entrypoint.sh index 9e4aea53..fe7abb4d 100644 --- a/prisma/entrypoint.sh +++ b/prisma/entrypoint.sh @@ -1,7 +1,7 @@ #!/bin/sh set -e -/app/wait-for-it.sh postgres:5432 -- echo Postgress is up! +./wait-for-it.sh postgres:5432 -- echo Postgress is up! echo Start applying migrations... @@ -11,4 +11,4 @@ npx prisma migrate deploy echo Seeding data... # seed data -npx ts-node seed.ts \ No newline at end of file +npx ts-node seed.ts diff --git a/prisma/package.json b/prisma/package.json index 5a8ed2d2..9c7ab60d 100644 --- a/prisma/package.json +++ b/prisma/package.json @@ -22,4 +22,4 @@ "ts-node": "^10.9.2", "typescript": "^5.9.3" } -} +} \ No newline at end of file diff --git a/prisma/tsconfig.json b/prisma/tsconfig.json index fc33cd97..e98be16d 100644 --- a/prisma/tsconfig.json +++ b/prisma/tsconfig.json @@ -11,6 +11,7 @@ "incremental": true, "skipLibCheck": true, "esModuleInterop": true, + "isolatedModules": true, // See: https://github.com/prisma/prisma/issues/10203 "strictNullChecks": true }, diff --git a/src/builds/builds.controller.ts b/src/builds/builds.controller.ts index 3637f8f9..8ad37931 100644 --- a/src/builds/builds.controller.ts +++ b/src/builds/builds.controller.ts @@ -23,7 +23,7 @@ import { Build, Role } from '@prisma/client'; import { BuildDto } from './dto/build.dto'; import { MixedGuard } from '../auth/guards/mixed.guard'; import { PaginatedBuildDto } from './dto/build-paginated.dto'; -import { ModifyBuildDto } from './dto/build-modify.dto'; +import type { ModifyBuildDto } from './dto/build-modify.dto'; import { ProjectsService } from '../projects/projects.service'; import { RoleGuard } from '../auth/guards/role.guard'; import { Roles } from '../shared/roles.decorator'; diff --git a/src/builds/builds.service.spec.ts b/src/builds/builds.service.spec.ts index 6a1ee5ee..510d7613 100644 --- a/src/builds/builds.service.spec.ts +++ b/src/builds/builds.service.spec.ts @@ -3,12 +3,11 @@ import { BuildsService } from './builds.service'; import { PrismaService } from '../prisma/prisma.service'; import { TestRunsService } from '../test-runs/test-runs.service'; import { EventsGateway } from '../shared/events/events.gateway'; -import { Build, TestRun, TestStatus } from '@prisma/client'; +import { Build, Prisma, TestRun, TestStatus } from '@prisma/client'; import { mocked, MockedObject } from 'jest-mock'; import { BuildDto } from './dto/build.dto'; import { ProjectsService } from '../projects/projects.service'; import { generateTestRun } from '../_data_'; -import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'; jest.mock('./dto/build.dto'); @@ -387,7 +386,9 @@ describe('BuildsService', () => { it('create with retry', async () => { const buildUpsertMock = jest .fn() - .mockRejectedValueOnce(new PrismaClientKnownRequestError('mock error', { code: 'P2002', clientVersion: '5' })); + .mockRejectedValueOnce( + new Prisma.PrismaClientKnownRequestError('mock error', { code: 'P2002', clientVersion: '5' }) + ); const buildUpdateMock = jest.fn().mockResolvedValueOnce(build); service = await initService({ buildUpsertMock, buildUpdateMock }); service.incrementBuildNumber = jest.fn().mockResolvedValueOnce(build); diff --git a/src/compare/libs/odiff/odiff.service.ts b/src/compare/libs/odiff/odiff.service.ts index e25c656d..5ec54afd 100644 --- a/src/compare/libs/odiff/odiff.service.ts +++ b/src/compare/libs/odiff/odiff.service.ts @@ -27,8 +27,11 @@ export class OdiffService implements ImageComparator { this.hddService = this.staticService as unknown as HddService; } - parseConfig(configJson: string): OdiffConfig { - return parseConfig(configJson, DEFAULT_CONFIG, this.logger); + parseConfig(configInput: string | OdiffConfig): OdiffConfig { + if (typeof configInput === 'string') { + return parseConfig(configInput, DEFAULT_CONFIG, this.logger); + } + return { ...DEFAULT_CONFIG, ...configInput }; } async getDiff(data: ImageCompareInput, config: OdiffConfig): Promise { diff --git a/src/compare/libs/pixelmatch/pixelmatch.service.spec.ts b/src/compare/libs/pixelmatch/pixelmatch.service.spec.ts index c458f82e..66e789ba 100644 --- a/src/compare/libs/pixelmatch/pixelmatch.service.spec.ts +++ b/src/compare/libs/pixelmatch/pixelmatch.service.spec.ts @@ -1,14 +1,19 @@ import { TestingModule, Test } from '@nestjs/testing'; import { TestStatus } from '@prisma/client'; -import Pixelmatch from 'pixelmatch'; import { PNG } from 'pngjs'; -import { mocked } from 'jest-mock'; import { StaticService } from '../../../static/static.service'; import { DIFF_DIMENSION_RESULT, EQUAL_RESULT, NO_BASELINE_RESULT } from '../consts'; import { DEFAULT_CONFIG, PixelmatchService } from './pixelmatch.service'; import { PixelmatchConfig } from './pixelmatch.types'; -jest.mock('pixelmatch'); +const mockPixelmatch = jest.fn(); + +// Helper to create a Uint8Array for a PNG of given dimensions +const createUint8ArrayForPng = (width: number, height: number): Uint8Array => { + const png = new PNG({ width, height }); + // Access the underlying buffer and create a Uint8Array view + return new Uint8Array(png.data.buffer, png.data.byteOffset, png.data.byteLength); +}; const initService = async ({ getImageMock = jest.fn(), saveImageMock = jest.fn(), deleteImageMock = jest.fn() }) => { const module: TestingModule = await Test.createTestingModule({ @@ -25,7 +30,12 @@ const initService = async ({ getImageMock = jest.fn(), saveImageMock = jest.fn() ], }).compile(); - return module.get(PixelmatchService); + const service = module.get(PixelmatchService); + + // Spy on loadPixelmatch to return our mock instead of dynamic import + jest.spyOn(service as any, 'loadPixelmatch').mockResolvedValue(mockPixelmatch); + + return service; }; let service: PixelmatchService; @@ -123,9 +133,15 @@ describe('getDiff', () => { const getImageMock = jest.fn().mockReturnValueOnce(image).mockReturnValueOnce(baseline); const diffName = 'diff name'; const saveImageMock = jest.fn().mockReturnValueOnce(diffName); - mocked(Pixelmatch).mockReturnValueOnce(5); + mockPixelmatch.mockReturnValueOnce(5); service = await initService({ saveImageMock, getImageMock }); + const testConfig = { + allowDiffDimensions: true, + ignoreAntialiasing: true, + threshold: 0.1, + }; + const result = await service.getDiff( { baseline: 'image', @@ -134,31 +150,18 @@ describe('getDiff', () => { ignoreAreas: [], saveDiffAsFile: true, }, - { - allowDiffDimensions: true, - ignoreAntialiasing: true, - threshold: 0.1, - } + testConfig ); - expect(mocked(Pixelmatch)).toHaveBeenCalledWith( - new PNG({ - width: 2, - height: 5, - }).data, - new PNG({ - width: 2, - height: 5, - }).data, - new PNG({ - width: 2, - height: 5, - }).data, + expect(mockPixelmatch).toHaveBeenCalledWith( + createUint8ArrayForPng(2, 5), + createUint8ArrayForPng(2, 5), + createUint8ArrayForPng(2, 5), 2, 5, { - includeAA: true, - threshold: 0.1, + includeAA: testConfig.ignoreAntialiasing, + threshold: testConfig.threshold, } ); expect(saveImageMock).toHaveBeenCalledTimes(1); @@ -185,7 +188,7 @@ describe('getDiff', () => { const saveImageMock = jest.fn(); service = await initService({ saveImageMock, getImageMock }); const pixelMisMatchCount = 150; - mocked(Pixelmatch).mockReturnValueOnce(pixelMisMatchCount); + mockPixelmatch.mockReturnValueOnce(pixelMisMatchCount); const result = await service.getDiff( { @@ -220,7 +223,7 @@ describe('getDiff', () => { }); const getImageMock = jest.fn().mockReturnValueOnce(image).mockReturnValueOnce(baseline); const pixelMisMatchCount = 200; - mocked(Pixelmatch).mockReturnValueOnce(pixelMisMatchCount); + mockPixelmatch.mockReturnValueOnce(pixelMisMatchCount); const diffName = 'diff name'; const saveImageMock = jest.fn().mockReturnValueOnce(diffName); service = await initService({ diff --git a/src/compare/libs/pixelmatch/pixelmatch.service.ts b/src/compare/libs/pixelmatch/pixelmatch.service.ts index 2689882b..3b2e04e4 100644 --- a/src/compare/libs/pixelmatch/pixelmatch.service.ts +++ b/src/compare/libs/pixelmatch/pixelmatch.service.ts @@ -1,6 +1,5 @@ import { Injectable, Logger } from '@nestjs/common'; import { TestStatus } from '@prisma/client'; -import Pixelmatch from 'pixelmatch'; import { PNG } from 'pngjs'; import { StaticService } from '../../../static/static.service'; import { DiffResult } from '../../../test-runs/diffResult'; @@ -18,11 +17,21 @@ export class PixelmatchService implements ImageComparator { constructor(private readonly staticService: StaticService) {} - parseConfig(configJson: string): PixelmatchConfig { - return parseConfig(configJson, DEFAULT_CONFIG, this.logger); + protected async loadPixelmatch() { + const { default: pixelmatch } = await import('pixelmatch'); + return pixelmatch; + } + + parseConfig(configInput: string | PixelmatchConfig): PixelmatchConfig { + if (typeof configInput === 'string') { + return parseConfig(configInput, DEFAULT_CONFIG, this.logger); + } + return { ...DEFAULT_CONFIG, ...configInput }; } async getDiff(data: ImageCompareInput, config: PixelmatchConfig): Promise { + const pixelmatch = await this.loadPixelmatch(); + const result: DiffResult = { ...NO_BASELINE_RESULT, }; @@ -58,10 +67,17 @@ export class PixelmatchService implements ImageComparator { width: maxWidth, height: maxHeight, }); - result.pixelMisMatchCount = Pixelmatch(baselineIgnored.data, imageIgnored.data, diff.data, maxWidth, maxHeight, { - includeAA: config.ignoreAntialiasing, - threshold: config.threshold, - }); + result.pixelMisMatchCount = pixelmatch( + new Uint8Array(baselineIgnored.data.buffer, baselineIgnored.data.byteOffset, baselineIgnored.data.byteLength), + new Uint8Array(imageIgnored.data.buffer, imageIgnored.data.byteOffset, imageIgnored.data.byteLength), + new Uint8Array(diff.data.buffer, diff.data.byteOffset, diff.data.byteLength), + maxWidth, + maxHeight, + { + includeAA: config.ignoreAntialiasing, + threshold: config.threshold, + } + ); result.diffPercent = (result.pixelMisMatchCount * 100) / (scaledImage.width * scaledImage.height); // process result diff --git a/src/jest.config.ts b/src/jest.config.ts index 287cef5d..6b2b9414 100644 --- a/src/jest.config.ts +++ b/src/jest.config.ts @@ -1,3 +1,6 @@ +/** @returns {Promise} */ +import { createDefaultPreset } from 'ts-jest'; + /** @returns {Promise} */ module.exports = async () => { return { @@ -5,8 +8,9 @@ module.exports = async () => { roots: ['./'], moduleFileExtensions: ['js', 'json', 'ts'], transform: { - '^.+\\.(t|j)s$': 'ts-jest', + ...createDefaultPreset().transform, }, + transformIgnorePatterns: ['node_modules/(?!(pixelmatch)/)'], coverageDirectory: '../coverage', testEnvironment: 'node', }; diff --git a/src/static/static.controller.ts b/src/static/static.controller.ts index 01bec303..e4f327aa 100644 --- a/src/static/static.controller.ts +++ b/src/static/static.controller.ts @@ -1,5 +1,5 @@ import { Controller, Get, Logger, Param, Res } from '@nestjs/common'; -import { Response } from 'express'; +import type { Response } from 'express'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { StaticService } from './static.service'; diff --git a/src/test-runs/dto/testRunResult.dto.ts b/src/test-runs/dto/testRunResult.dto.ts index a4364424..a7f79061 100644 --- a/src/test-runs/dto/testRunResult.dto.ts +++ b/src/test-runs/dto/testRunResult.dto.ts @@ -7,9 +7,6 @@ export class TestRunResultDto extends TestRunDto { pixelMisMatchCount?: number; @ApiProperty() url: string; - @ApiProperty() - baselineName: string; - constructor(testRun: TestRun, testVariation: TestVariation) { super(testRun); this.baselineName = testVariation.baselineName; diff --git a/src/test-runs/test-runs.controller.ts b/src/test-runs/test-runs.controller.ts index 7796d0ec..52bf5e70 100644 --- a/src/test-runs/test-runs.controller.ts +++ b/src/test-runs/test-runs.controller.ts @@ -25,7 +25,8 @@ import { ApiBody, } from '@nestjs/swagger'; import { JwtAuthGuard } from '../auth/guards/auth.guard'; -import { Role, TestRun, TestStatus, User } from '@prisma/client'; +import { Role, TestRun, TestStatus } from '@prisma/client'; +import type { User } from '@prisma/client'; import { TestRunsService } from './test-runs.service'; import { TestRunResultDto } from './dto/testRunResult.dto'; import { ApiGuard } from '../auth/guards/api.guard'; diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index b3cfb80f..ef517ba6 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -8,7 +8,8 @@ import { UserDto } from './dto/user.dto'; import { UpdateUserDto } from './dto/user-update.dto'; import { UserLoginRequestDto } from './dto/user-login-request.dto'; import { CurrentUser } from '../shared/current-user.decorator'; -import { Role, User } from '@prisma/client'; +import { Role } from '@prisma/client'; +import type { User } from '@prisma/client'; import { RoleGuard } from '../auth/guards/role.guard'; import { Roles } from '../shared/roles.decorator'; import { PrismaService } from '../prisma/prisma.service'; diff --git a/test/jest.config.ts b/test/jest.config.ts index 86edeb98..9b01c0ed 100644 --- a/test/jest.config.ts +++ b/test/jest.config.ts @@ -10,6 +10,7 @@ module.exports = async () => { transform: { ...createDefaultPreset().transform, }, + transformIgnorePatterns: ['node_modules/(?!(pixelmatch)/)'], coverageDirectory: '../coverage', testEnvironment: 'node', }; diff --git a/test_acceptance/jest.config.ts b/test_acceptance/jest.config.ts index c5a70de6..ea394481 100644 --- a/test_acceptance/jest.config.ts +++ b/test_acceptance/jest.config.ts @@ -1,3 +1,5 @@ +import { createDefaultPreset } from 'ts-jest'; + /** @returns {Promise} */ module.exports = async () => { return { @@ -7,8 +9,9 @@ module.exports = async () => { testRegex: '.spec.ts$', moduleFileExtensions: ['js', 'json', 'ts'], transform: { - '^.+\\.(t|j)s$': 'ts-jest', + ...createDefaultPreset().transform, }, + transformIgnorePatterns: ['node_modules/(?!(pixelmatch)/)'], testEnvironment: 'node', }; }; diff --git a/test_ldap/jest.config.ts b/test_ldap/jest.config.ts index a7ec3362..3b4274aa 100644 --- a/test_ldap/jest.config.ts +++ b/test_ldap/jest.config.ts @@ -1,3 +1,5 @@ +import { createDefaultPreset } from 'ts-jest'; + /** @returns {Promise} */ module.exports = async () => { return { @@ -7,8 +9,9 @@ module.exports = async () => { testRegex: '.spec.ts$', moduleFileExtensions: ['js', 'json', 'ts'], transform: { - '^.+\\.(t|j)s$': 'ts-jest', + ...createDefaultPreset().transform, }, + transformIgnorePatterns: ['node_modules/(?!(pixelmatch)/)'], testEnvironment: 'node', }; }; diff --git a/tsconfig.build.json b/tsconfig.build.json index 64f86c6b..5744c52d 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,4 +1,7 @@ { "extends": "./tsconfig.json", - "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] + "compilerOptions": { + "target": "ESNext" + }, + "exclude": ["node_modules", "test", "dist", "**/*spec.ts", "prisma"] } diff --git a/tsconfig.json b/tsconfig.json index 140e7c3b..e8d0b238 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,8 @@ "baseUrl": "./", "incremental": true, "skipLibCheck": true, - "esModuleInterop": true + "esModuleInterop": true, + "isolatedModules": true }, "exclude": ["node_modules", "dist"] }