Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,112 changes: 928 additions & 184 deletions package-lock.json

Large diffs are not rendered by default.

13 changes: 10 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
"preview": "vite preview",
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage"
},
"dependencies": {
"@preact/signals": "^1.3.0",
Expand All @@ -14,7 +17,6 @@
"dompurify": "^3.1.6",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"jsdom": "^25.0.0",
"preact": "^10.22.1",
"preact-iso": "^2.6.3",
"react-bpmn": "^0.2.0",
Expand All @@ -23,11 +25,16 @@
},
"devDependencies": {
"@preact/preset-vite": "^2.9.0",
"@testing-library/preact": "^3.2.4",
"@vitest/ui": "^4.0.7",
"eslint": "^8.57.0",
"eslint-config-preact": "^1.4.0",
"happy-dom": "^20.0.10",
"jest": "^29.7.0",
"jsdom": "^27.0.1",
"prettier": "3.6.2",
"vite": "^7.0.0"
"vite": "^7.0.0",
"vitest": "^4.0.7"
},
"eslintConfig": {
"extends": "preact"
Expand Down
96 changes: 96 additions & 0 deletions src/api/helper.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { describe, it, expect } from 'vitest';
import { _url_server, _url_engine_rest, get_credentials, RESPONSE_STATE } from './helper.jsx';

// Test helper functions
const createMockState = (serverUrl, credentials = { username: 'demo', password: 'demo' }) => ({
server: {
value: {
url: serverUrl
}
},
auth: {
credentials
}
});

describe('api/helper', () => {
describe('URL builders', () => {
it('should build server URL from state', () => {
const state = createMockState('http://localhost:8080');
const result = _url_server(state);
expect(result).toBe('http://localhost:8080');
});

it('should build engine REST URL from state', () => {
const state = createMockState('http://localhost:8080');
const result = _url_engine_rest(state);
expect(result).toBe('http://localhost:8080/engine-rest');
});

it('should handle different server URLs', () => {
const state = createMockState('https://example.com:9090');
expect(_url_server(state)).toBe('https://example.com:9090');
expect(_url_engine_rest(state)).toBe('https://example.com:9090/engine-rest');
});

it('should handle URLs without trailing slash', () => {
const state = createMockState('http://127.0.0.1:5173');
expect(_url_engine_rest(state)).toBe('http://127.0.0.1:5173/engine-rest');
});
});

describe('get_credentials', () => {
it('should format credentials as username:password', () => {
const state = createMockState('http://localhost:8080', { username: 'demo', password: 'demo' });
const result = get_credentials(state);
expect(result).toBe('demo:demo');
});

it('should handle different credentials', () => {
const state = createMockState('http://localhost:8080', { username: 'admin', password: 'secret123' });
const result = get_credentials(state);
expect(result).toBe('admin:secret123');
});

it('should handle special characters in credentials', () => {
const state = createMockState('http://localhost:8080', { username: '[email protected]', password: 'p@ssw0rd!' });
const result = get_credentials(state);
expect(result).toBe('[email protected]:p@ssw0rd!');
});

it('should handle empty credentials', () => {
const state = createMockState('http://localhost:8080', { username: '', password: '' });
const result = get_credentials(state);
expect(result).toBe(':');
});
});

describe('RESPONSE_STATE constants', () => {
it('should have NOT_INITIALIZED state', () => {
expect(RESPONSE_STATE.NOT_INITIALIZED).toBe('NOT_INITIALIZED');
});

it('should have LOADING state', () => {
expect(RESPONSE_STATE.LOADING).toBe('LOADING');
});

it('should have SUCCESS state', () => {
expect(RESPONSE_STATE.SUCCESS).toBe('SUCCESS');
});

it('should have ERROR state', () => {
expect(RESPONSE_STATE.ERROR).toBe('ERROR');
});

it('should have exactly 4 states', () => {
const states = Object.keys(RESPONSE_STATE);
expect(states).toHaveLength(4);
});

it('should have unique state values', () => {
const values = Object.values(RESPONSE_STATE);
const uniqueValues = new Set(values);
expect(uniqueValues.size).toBe(values.length);
});
});
});
3 changes: 2 additions & 1 deletion src/helper/date_formatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ const formatRelativeDate = (date) => {

// returns date/time relative formatted, e.g. yesterday, 2 hours ago, 3 month ago, tomorrow, in 5 minutes, ...
const formatRelativeDateTime = (date, ignoreTime) => {
const timeFormatter = new Intl.RelativeTimeFormat('en', { numeric: 'auto' }); // TODO locale
const locale = navigator.language || navigator.languages?.[0] || 'en';
const timeFormatter = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
let relativeDate = new Date(date);
let nowDate = new Date();

Expand Down
135 changes: 135 additions & 0 deletions src/helper/date_formatter.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { formatRelativeDate, formatRelativeDateTime } from './date_formatter.js';

describe('date_formatter', () => {
let originalNavigator;

beforeEach(() => {
// Save original navigator
originalNavigator = global.navigator;
});

afterEach(() => {
// Restore original navigator
global.navigator = originalNavigator;
vi.useRealTimers();
});

describe('formatRelativeDate', () => {
it('should format today as "today"', () => {
const today = new Date();
const result = formatRelativeDate(today);
expect(result).toBe('today');
});

it('should format yesterday as "yesterday"', () => {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const result = formatRelativeDate(yesterday);
expect(result).toBe('yesterday');
});

it('should format tomorrow as "tomorrow"', () => {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const result = formatRelativeDate(tomorrow);
expect(result).toBe('tomorrow');
});

it('should format past dates in days', () => {
const threeDaysAgo = new Date();
threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);
const result = formatRelativeDate(threeDaysAgo);
expect(result).toBe('3 days ago');
});

it('should format future dates in days', () => {
const inFiveDays = new Date();
inFiveDays.setDate(inFiveDays.getDate() + 5);
const result = formatRelativeDate(inFiveDays);
expect(result).toBe('in 5 days');
});

it('should format dates more than a week ago in weeks', () => {
const twoWeeksAgo = new Date();
twoWeeksAgo.setDate(twoWeeksAgo.getDate() - 14);
const result = formatRelativeDate(twoWeeksAgo);
expect(result).toContain('week');
});
});

describe('formatRelativeDateTime', () => {
it('should format times a few seconds ago', () => {
const fiveSecondsAgo = new Date();
fiveSecondsAgo.setSeconds(fiveSecondsAgo.getSeconds() - 5);
const result = formatRelativeDateTime(fiveSecondsAgo, false);
expect(result).toBe('5 seconds ago');
});

it('should format times in minutes', () => {
const tenMinutesAgo = new Date();
tenMinutesAgo.setMinutes(tenMinutesAgo.getMinutes() - 10);
const result = formatRelativeDateTime(tenMinutesAgo, false);
expect(result).toBe('10 minutes ago');
});

it('should format times in hours', () => {
const twoHoursAgo = new Date();
twoHoursAgo.setHours(twoHoursAgo.getHours() - 2);
const result = formatRelativeDateTime(twoHoursAgo, false);
expect(result).toBe('2 hours ago');
});

it('should ignore time when ignoreTime is true', () => {
const todayWithDifferentTime = new Date();
todayWithDifferentTime.setHours(todayWithDifferentTime.getHours() - 2);
const result = formatRelativeDateTime(todayWithDifferentTime, true);
expect(result).toBe('today');
});
});

describe('locale support', () => {
it('should use browser locale from navigator.language', () => {
// Mock navigator.language to German
global.navigator = {
language: 'de-DE',
languages: ['de-DE', 'en-US'],
};

const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const result = formatRelativeDate(yesterday);

// German locale should return "gestern" for yesterday
expect(result).toBe('gestern');
});

it('should fallback to navigator.languages[0] if language is not set', () => {
global.navigator = {
language: undefined,
languages: ['fr-FR', 'en-US'],
};

const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const result = formatRelativeDate(yesterday);

// French locale should return "hier" for yesterday
expect(result).toBe('hier');
});

it('should fallback to "en" if no locale is available', () => {
global.navigator = {
language: undefined,
languages: undefined,
};

const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const result = formatRelativeDate(yesterday);

// English fallback should return "yesterday"
expect(result).toBe('yesterday');
});
});
});
12 changes: 9 additions & 3 deletions src/pages/TaskForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,15 @@ const parse_html = (state, html) => {
if (field.getAttribute('cam-variable-type') === 'Long') field.type = 'number'
if (disable) field.setAttribute('disabled', 'disabled')
if (field.hasAttribute('required')) {
//TODO: Muss noch gemacht werden
if (field.type !== 'date') {
field.previousElementSibling.textContent += '*'
// Mark required fields with asterisk
// Try to find label: either previous sibling or parent label (for nested inputs)
const prevElement = field.previousElementSibling
const parentLabel = field.closest('label')

if (prevElement && prevElement.tagName === 'LABEL' && !prevElement.textContent.includes('*')) {
prevElement.textContent += '*'
} else if (parentLabel && !parentLabel.textContent.includes('*')) {
parentLabel.textContent += '*'
}
}

Expand Down
Loading