Merge pull request 'fix(deploy): new test' (#14) from prova into dev
All checks were successful
Build and Deploy / test-backend (push) Successful in 26s
PR Checks / security-sast (pull_request) Successful in 32s
Build and Deploy / test-frontend (push) Successful in 1m3s
PR Checks / test-backend (pull_request) Successful in 27s
Build and Deploy / build-and-push (push) Successful in 25s
PR Checks / test-frontend (pull_request) Successful in 1m4s
Build and Deploy / deploy (push) Successful in 13s
PR Checks / prettier-autofix (pull_request) Successful in 10s
All checks were successful
Build and Deploy / test-backend (push) Successful in 26s
PR Checks / security-sast (pull_request) Successful in 32s
Build and Deploy / test-frontend (push) Successful in 1m3s
PR Checks / test-backend (pull_request) Successful in 27s
Build and Deploy / build-and-push (push) Successful in 25s
PR Checks / test-frontend (pull_request) Successful in 1m4s
Build and Deploy / deploy (push) Successful in 13s
PR Checks / prettier-autofix (pull_request) Successful in 10s
Reviewed-on: #14
This commit was merged in pull request #14.
This commit is contained in:
@@ -28,8 +28,42 @@ jobs:
|
||||
chmod +x gradlew
|
||||
./gradlew test
|
||||
|
||||
test-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node 22
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
cache-dependency-path: "frontend/package-lock.json"
|
||||
|
||||
- name: Install Chromium
|
||||
shell: bash
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends chromium
|
||||
|
||||
- name: Install frontend dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
cd frontend
|
||||
npm ci --no-audit --no-fund
|
||||
|
||||
- name: Run frontend tests (headless)
|
||||
shell: bash
|
||||
env:
|
||||
CHROME_BIN: /usr/bin/chromium
|
||||
CI: "true"
|
||||
run: |
|
||||
cd frontend
|
||||
npm run test -- --watch=false --browsers=ChromeHeadlessNoSandbox
|
||||
|
||||
build-and-push:
|
||||
needs: test-backend
|
||||
needs: [test-backend, test-frontend]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
@@ -136,3 +136,37 @@ jobs:
|
||||
cd backend
|
||||
chmod +x gradlew
|
||||
./gradlew test
|
||||
|
||||
test-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node 22
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
cache-dependency-path: "frontend/package-lock.json"
|
||||
|
||||
- name: Install Chromium
|
||||
shell: bash
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends chromium
|
||||
|
||||
- name: Install frontend dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
cd frontend
|
||||
npm ci --no-audit --no-fund
|
||||
|
||||
- name: Run frontend tests (headless)
|
||||
shell: bash
|
||||
env:
|
||||
CHROME_BIN: /usr/bin/chromium
|
||||
CI: "true"
|
||||
run: |
|
||||
cd frontend
|
||||
npm run test -- --watch=false --browsers=ChromeHeadlessNoSandbox
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
|
||||
40
frontend/karma.conf.js
Normal file
40
frontend/karma.conf.js
Normal file
@@ -0,0 +1,40 @@
|
||||
// Karma config dedicated to CI-safe Chrome execution.
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage'),
|
||||
require('@angular-devkit/build-angular/plugins/karma'),
|
||||
],
|
||||
client: {
|
||||
jasmine: {},
|
||||
clearContext: false,
|
||||
},
|
||||
jasmineHtmlReporter: {
|
||||
suppressAll: true,
|
||||
},
|
||||
coverageReporter: {
|
||||
dir: require('path').join(__dirname, './coverage/frontend'),
|
||||
subdir: '.',
|
||||
reporters: [{ type: 'html' }, { type: 'text-summary' }],
|
||||
},
|
||||
reporters: ['progress', 'kjhtml'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['ChromeHeadlessNoSandbox'],
|
||||
customLaunchers: {
|
||||
ChromeHeadlessNoSandbox: {
|
||||
base: 'ChromeHeadless',
|
||||
flags: ['--no-sandbox', '--disable-dev-shm-usage'],
|
||||
},
|
||||
},
|
||||
singleRun: false,
|
||||
restartOnFileChange: true,
|
||||
});
|
||||
};
|
||||
106
frontend/src/app/core/services/language.service.spec.ts
Normal file
106
frontend/src/app/core/services/language.service.spec.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { Subject } from 'rxjs';
|
||||
import { DefaultUrlSerializer, Router, UrlTree } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { LanguageService } from './language.service';
|
||||
|
||||
describe('LanguageService', () => {
|
||||
function createTranslateMock() {
|
||||
const onLangChange = new Subject<{ lang: string }>();
|
||||
const translate = {
|
||||
currentLang: '',
|
||||
addLangs: jasmine.createSpy('addLangs'),
|
||||
setDefaultLang: jasmine.createSpy('setDefaultLang'),
|
||||
use: jasmine.createSpy('use').and.callFake((lang: string) => {
|
||||
translate.currentLang = lang;
|
||||
onLangChange.next({ lang });
|
||||
}),
|
||||
onLangChange,
|
||||
};
|
||||
|
||||
return translate as unknown as TranslateService;
|
||||
}
|
||||
|
||||
function createRouterMock(initialUrl: string) {
|
||||
const serializer = new DefaultUrlSerializer();
|
||||
const events$ = new Subject<unknown>();
|
||||
|
||||
const createUrlTree = (
|
||||
commands: unknown[],
|
||||
extras?: { queryParams?: Record<string, string>; fragment?: string },
|
||||
): UrlTree => {
|
||||
const segments = commands
|
||||
.filter((entry) => typeof entry === 'string' && entry !== '/')
|
||||
.map((entry) => String(entry));
|
||||
|
||||
let url = `/${segments.join('/')}`;
|
||||
if (url === '') {
|
||||
url = '/';
|
||||
}
|
||||
|
||||
const queryParams = extras?.queryParams ?? {};
|
||||
const query = new URLSearchParams();
|
||||
Object.entries(queryParams).forEach(([key, value]) => {
|
||||
query.set(key, value);
|
||||
});
|
||||
const queryString = query.toString();
|
||||
if (queryString) {
|
||||
url += `?${queryString}`;
|
||||
}
|
||||
|
||||
if (extras?.fragment) {
|
||||
url += `#${extras.fragment}`;
|
||||
}
|
||||
|
||||
return serializer.parse(url);
|
||||
};
|
||||
|
||||
const router = {
|
||||
url: initialUrl,
|
||||
events: events$.asObservable(),
|
||||
parseUrl: (url: string) => serializer.parse(url),
|
||||
createUrlTree,
|
||||
serializeUrl: (tree: UrlTree) => serializer.serialize(tree),
|
||||
navigateByUrl: jasmine.createSpy('navigateByUrl'),
|
||||
};
|
||||
|
||||
return router as unknown as Router;
|
||||
}
|
||||
|
||||
it('prefixes URL with default language when missing', () => {
|
||||
const translate = createTranslateMock();
|
||||
const router = createRouterMock('/calculator?session=abc');
|
||||
const navigateSpy = router.navigateByUrl as unknown as jasmine.Spy;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const service = new LanguageService(translate, router);
|
||||
|
||||
expect(translate.use).toHaveBeenCalledWith('it');
|
||||
expect(navigateSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
const firstCall = navigateSpy.calls.mostRecent();
|
||||
const tree = firstCall.args[0] as UrlTree;
|
||||
const navOptions = firstCall.args[1] as { replaceUrl: boolean };
|
||||
expect(router.serializeUrl(tree)).toBe('/it/calculator?session=abc');
|
||||
expect(navOptions.replaceUrl).toBeTrue();
|
||||
});
|
||||
|
||||
it('switches language while preserving path and query params', () => {
|
||||
const translate = createTranslateMock();
|
||||
const router = createRouterMock('/it/calculator?session=abc&mode=advanced');
|
||||
const navigateSpy = router.navigateByUrl as unknown as jasmine.Spy;
|
||||
const service = new LanguageService(translate, router);
|
||||
|
||||
expect(navigateSpy).not.toHaveBeenCalled();
|
||||
|
||||
service.switchLang('de');
|
||||
|
||||
expect(translate.use).toHaveBeenCalledWith('de');
|
||||
expect(navigateSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
const call = navigateSpy.calls.mostRecent();
|
||||
const tree = call.args[0] as UrlTree;
|
||||
expect(router.serializeUrl(tree)).toBe(
|
||||
'/de/calculator?session=abc&mode=advanced',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
import { of } from 'rxjs';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { CalculatorPageComponent } from './calculator-page.component';
|
||||
import {
|
||||
QuoteEstimatorService,
|
||||
QuoteResult,
|
||||
} from './services/quote-estimator.service';
|
||||
import { LanguageService } from '../../core/services/language.service';
|
||||
import { UploadFormComponent } from './components/upload-form/upload-form.component';
|
||||
|
||||
describe('CalculatorPageComponent', () => {
|
||||
const createResult = (sessionId: string, notes?: string): QuoteResult => ({
|
||||
sessionId,
|
||||
items: [
|
||||
{
|
||||
id: 'line-1',
|
||||
fileName: 'part-a.stl',
|
||||
unitPrice: 4,
|
||||
unitTime: 120,
|
||||
unitWeight: 2,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
setupCost: 2,
|
||||
globalMachineCost: 0,
|
||||
currency: 'CHF',
|
||||
totalPrice: 6,
|
||||
totalTimeHours: 0,
|
||||
totalTimeMinutes: 2,
|
||||
totalWeight: 2,
|
||||
notes,
|
||||
});
|
||||
|
||||
function createComponent() {
|
||||
const estimator = jasmine.createSpyObj<QuoteEstimatorService>(
|
||||
'QuoteEstimatorService',
|
||||
['updateLineItem', 'getQuoteSession', 'mapSessionToQuoteResult'],
|
||||
);
|
||||
const router = jasmine.createSpyObj<Router>('Router', ['navigate']);
|
||||
const route = {
|
||||
data: of({}),
|
||||
queryParams: of({}),
|
||||
} as unknown as ActivatedRoute;
|
||||
const languageService = jasmine.createSpyObj<LanguageService>(
|
||||
'LanguageService',
|
||||
['selectedLang'],
|
||||
);
|
||||
|
||||
const component = new CalculatorPageComponent(
|
||||
estimator,
|
||||
router,
|
||||
route,
|
||||
languageService,
|
||||
);
|
||||
|
||||
const uploadForm = jasmine.createSpyObj<UploadFormComponent>(
|
||||
'UploadFormComponent',
|
||||
['updateItemQuantityByIndex', 'updateItemQuantityByName'],
|
||||
);
|
||||
component.uploadForm = uploadForm;
|
||||
|
||||
return {
|
||||
component,
|
||||
estimator,
|
||||
uploadForm,
|
||||
};
|
||||
}
|
||||
|
||||
it('updates left panel quantities even when item id is missing', () => {
|
||||
const { component, estimator, uploadForm } = createComponent();
|
||||
|
||||
component.onItemChange({
|
||||
index: 0,
|
||||
fileName: 'part-a.stl',
|
||||
quantity: 4,
|
||||
});
|
||||
|
||||
expect(uploadForm.updateItemQuantityByIndex).toHaveBeenCalledWith(0, 4);
|
||||
expect(uploadForm.updateItemQuantityByName).toHaveBeenCalledWith(
|
||||
'part-a.stl',
|
||||
4,
|
||||
);
|
||||
expect(estimator.updateLineItem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('refreshes quote totals after successful line item update', () => {
|
||||
const { component, estimator } = createComponent();
|
||||
component.result.set(createResult('session-1', 'persisted notes'));
|
||||
|
||||
estimator.updateLineItem.and.returnValue(of({ ok: true }));
|
||||
estimator.getQuoteSession.and.returnValue(
|
||||
of({ session: { id: 'session-1' } }),
|
||||
);
|
||||
estimator.mapSessionToQuoteResult.and.returnValue(
|
||||
createResult('session-1'),
|
||||
);
|
||||
|
||||
component.onItemChange({
|
||||
id: 'line-1',
|
||||
index: 0,
|
||||
fileName: 'part-a.stl',
|
||||
quantity: 7,
|
||||
});
|
||||
|
||||
expect(estimator.updateLineItem).toHaveBeenCalledWith('line-1', {
|
||||
quantity: 7,
|
||||
});
|
||||
expect(estimator.getQuoteSession).toHaveBeenCalledWith('session-1');
|
||||
expect(component.result()?.notes).toBe('persisted notes');
|
||||
expect(component.result()?.items[0].quantity).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { QuoteResultComponent } from './quote-result.component';
|
||||
import { QuoteResult } from '../../services/quote-estimator.service';
|
||||
|
||||
describe('QuoteResultComponent', () => {
|
||||
let fixture: ComponentFixture<QuoteResultComponent>;
|
||||
let component: QuoteResultComponent;
|
||||
|
||||
const createResult = (): QuoteResult => ({
|
||||
sessionId: 'session-1',
|
||||
items: [
|
||||
{
|
||||
id: 'line-1',
|
||||
fileName: 'part-a.stl',
|
||||
unitPrice: 2,
|
||||
unitTime: 120,
|
||||
unitWeight: 1.2,
|
||||
quantity: 2,
|
||||
},
|
||||
{
|
||||
id: 'line-2',
|
||||
fileName: 'part-b.stl',
|
||||
unitPrice: 1.5,
|
||||
unitTime: 60,
|
||||
unitWeight: 0.5,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
setupCost: 5,
|
||||
globalMachineCost: 0,
|
||||
currency: 'CHF',
|
||||
totalPrice: 0,
|
||||
totalTimeHours: 0,
|
||||
totalTimeMinutes: 0,
|
||||
totalWeight: 0,
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [QuoteResultComponent, TranslateModule.forRoot()],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(QuoteResultComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.componentRef.setInput('result', createResult());
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('emits quantity changes with clamped max quantity', () => {
|
||||
spyOn(component.itemChange, 'emit');
|
||||
|
||||
component.updateQuantity(0, 999);
|
||||
component.flushQuantityUpdate(0);
|
||||
|
||||
expect(component.items()[0].quantity).toBe(component.maxInputQuantity);
|
||||
expect(component.itemChange.emit).toHaveBeenCalledWith({
|
||||
id: 'line-1',
|
||||
index: 0,
|
||||
fileName: 'part-a.stl',
|
||||
quantity: component.maxInputQuantity,
|
||||
});
|
||||
});
|
||||
|
||||
it('computes totals from local item quantities', () => {
|
||||
component.updateQuantity(1, 3);
|
||||
|
||||
const totals = component.totals();
|
||||
expect(totals.price).toBe(13.5);
|
||||
expect(totals.hours).toBe(0);
|
||||
expect(totals.minutes).toBe(7);
|
||||
expect(totals.weight).toBe(4);
|
||||
});
|
||||
|
||||
it('flags over-limit quantities for direct order', () => {
|
||||
component.updateQuantity(0, 101);
|
||||
expect(component.hasQuantityOverLimit()).toBeTrue();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user