-
Notifications
You must be signed in to change notification settings - Fork 60
[UIK-3464][time-picker] rewrote component to ts/refactoring time picker component/added unit tests for core time picker elements #2641
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: release/v17
Are you sure you want to change the base?
Changes from all commits
2190694
8fea293
da43009
829f8ba
a2ee282
df8bbce
9d403cd
37f1e68
1357916
a15eb8c
efc2f96
f0715f3
647997e
21779f9
6c80b3f
d4babde
c3af396
24d9d07
822c5fe
e80a828
2fc4135
11d7394
199b49f
3e555a6
6630411
3dc1d2d
18b5dd4
dd31537
633823c
1b976c4
63df0d2
ad658b0
442740d
9889e54
ba7b78f
3fb65a6
52d9a82
ceebe7d
6166d2e
e19f026
dd888b9
ec1a4dc
379bdc7
5e09c3d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| type WatchedProps<Props> = { [key in keyof Props]?: unknown }; | ||
|
|
||
| type Constructor<Props> = new (...args: any[]) => { | ||
| props: Props; | ||
| onPropsChange(changedProps: WatchedProps<Props>): void; | ||
| render(): React.ReactNode; | ||
| componentWillUnmount?(): void; | ||
| }; | ||
|
|
||
| function propsObserver< | ||
| P extends Record<string, any>, | ||
| C extends Constructor<P>, | ||
| >(propsToWatch: Array<keyof P>) { | ||
| return function (Class: C): C & Constructor<P> { | ||
| return class extends Class { | ||
| __observableProps: WatchedProps<P> = {}; | ||
|
|
||
| constructor(...args: any[]) { | ||
| super(...args); | ||
|
|
||
| if (!this.props) return; | ||
|
|
||
| const observablePropKeys = propsToWatch.length === 0 ? Object.keys(this.props) : [...propsToWatch]; | ||
|
|
||
| observablePropKeys.forEach((key) => { | ||
| this.__observableProps[key] = this.props[key]; | ||
| }); | ||
ilyabrower marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| onPropsChange(_?: WatchedProps<P>) { | ||
| let shouldEmitChanges = false; | ||
| const changedProps: WatchedProps<P> = {}; | ||
|
|
||
| Object.entries(this.__observableProps).forEach(([key, value]: [key: keyof P, value: unknown]) => { | ||
| const arePropsEqual = Object.is(value, this.props[key]); | ||
|
|
||
| if (!arePropsEqual) { | ||
| this.__observableProps[key] = this.props[key]; | ||
| changedProps[key] = this.props[key]; | ||
|
|
||
| shouldEmitChanges = true; | ||
| } | ||
| }); | ||
|
|
||
| if (!shouldEmitChanges) return; | ||
|
|
||
| super.onPropsChange(changedProps); | ||
| } | ||
|
|
||
| componentWillUnmount() { | ||
| super.componentWillUnmount?.(); | ||
|
|
||
| Object.keys(this.__observableProps).forEach((key: keyof P) => { | ||
| this.__observableProps[key] = undefined; | ||
| }); | ||
| } | ||
|
|
||
| render() { | ||
| this.onPropsChange(); | ||
|
|
||
| return super.render(); | ||
| } | ||
| }; | ||
| }; | ||
| } | ||
|
|
||
| export default propsObserver; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| type Primitive = string | number | boolean | symbol | bigint | null | undefined; | ||
| type IsReadonly<This, Property extends keyof This> = | ||
| (<F>() => F extends { [P in Property]: This[Property] } ? 1 : 2) extends | ||
| (<F>() => F extends { -readonly [P in Property]: This[Property] } ? 1 : 2) | ||
| ? false | ||
| : true; | ||
| type Callback<This> = (this: This, field: string | symbol, newValue: unknown) => void; | ||
ilyabrower marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| type ReturnType< | ||
| This, | ||
| Property extends keyof This = keyof This, | ||
| > = IsReadonly<This, Property> extends true | ||
| ? (_: undefined, ctx: ClassFieldDecoratorContext<This, This[Property]>) => void | ||
| : This[Property] extends Primitive | ||
| ? (_: undefined, ctx: ClassFieldDecoratorContext<This, This[Property]>) => void | ||
| : never; | ||
|
|
||
| const isPrimitiveValue = (value: unknown) => value !== Object(value); | ||
|
|
||
| function reactive< | ||
| This, | ||
| Property extends keyof This, | ||
| Value = This[Property], | ||
| >(cb: Value extends Primitive ? Callback<This> : never): ReturnType<This>; | ||
| function reactive< | ||
| This, | ||
| Property extends keyof This, | ||
| Value = This[Property], | ||
| >(watchedFields: Value extends Primitive ? never : Array<keyof Value>, cb: Callback<This>): ReturnType<This>; | ||
|
|
||
| function reactive< | ||
| This, | ||
| Property extends keyof This, | ||
| >(watchedFieldsOrCb: Array<keyof This[Property]> | Callback<This>, cb?: Callback<This>) { | ||
| return function (_: undefined, ctx: ClassFieldDecoratorContext<This, This[Property]>) { | ||
| const { addInitializer, name } = ctx; | ||
|
|
||
| addInitializer(function (this: This) { | ||
| const thisRoot = this; | ||
|
|
||
| const isPrimitive = isPrimitiveValue(this[name as Property]); | ||
|
|
||
| const callback = typeof watchedFieldsOrCb === 'function' ? watchedFieldsOrCb : cb!; | ||
| const fields = Array.isArray(watchedFieldsOrCb) ? watchedFieldsOrCb : null; | ||
|
|
||
| if (isPrimitive) { | ||
| let value = this[name as Property]; | ||
|
|
||
| Object.defineProperty(this, name, { | ||
| get() { | ||
| return value; | ||
| }, | ||
| set(newValue) { | ||
| const oldValue = value; | ||
|
|
||
| value = newValue; | ||
|
|
||
| if (oldValue !== newValue) { | ||
| callback.call(thisRoot, name, newValue); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd like to talk about thisRoot
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. discussed |
||
| } | ||
| }, | ||
| enumerable: true, | ||
| configurable: true, | ||
| }); | ||
| } else { | ||
| // @ts-ignore | ||
| this[name] = new Proxy(this[name], { | ||
| set(target, p, newValue) { | ||
| target[p] = newValue; | ||
|
|
||
| if (fields?.length === 0 || fields?.includes(p as keyof This[Property])) { | ||
| callback.call(thisRoot, p, newValue); | ||
| } | ||
|
|
||
| return true; | ||
| }, | ||
| }); | ||
| } | ||
| }); | ||
| }; | ||
| } | ||
|
|
||
| export default reactive; | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| import { runDependencyCheckTests } from '@semcore/testing-utils/shared-tests'; | ||
| import { describe, it, expect } from '@semcore/testing-utils/vitest'; | ||
|
|
||
| import TimePickerEntity from '../src/entity/TimePickerEntity'; | ||
|
|
||
| describe('time-picker Dependency imports', () => { | ||
| runDependencyCheckTests('time-picker'); | ||
| }); | ||
|
|
||
| describe('TimePickerEntity', () => { | ||
| describe('constructor', () => { | ||
| it('should initialize with default empty time when no value provided', () => { | ||
| const entity = new TimePickerEntity(':'); | ||
|
|
||
| expect(entity.hours).toBe(''); | ||
| expect(entity.minutes).toBe(''); | ||
| }); | ||
|
|
||
| it('should parse hours and minutes from value string', () => { | ||
| const entity = new TimePickerEntity('14:30'); | ||
|
|
||
| expect(entity.hours).toBe('14'); | ||
| expect(entity.minutes).toBe('30'); | ||
| }); | ||
|
|
||
| it('should handle single digit hours and minutes', () => { | ||
| const entity = new TimePickerEntity('9:5'); | ||
|
|
||
| expect(entity.hours).toBe('09'); | ||
| expect(entity.minutes).toBe('05'); | ||
| }); | ||
|
|
||
| it('should initialize with AM meridiem by default', () => { | ||
| const entity = new TimePickerEntity('10:00', true); | ||
|
|
||
| expect(entity.meridiem).toBe('AM'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('12-hour format', () => { | ||
| it('should format midnight (00:00) as 12:00 AM', () => { | ||
| const entity = new TimePickerEntity('0:00', true); | ||
|
|
||
| expect(entity.hours).toBe('12'); | ||
| expect(entity.minutes).toBe('00'); | ||
| }); | ||
|
|
||
| it('should format hours 1-11 AM correctly', () => { | ||
| const entity = new TimePickerEntity('9:30', true); | ||
|
|
||
| expect(entity.hours).toBe('09'); | ||
| }); | ||
|
|
||
| it('should format noon (12:00) as 12:00', () => { | ||
| const entity = new TimePickerEntity('12:00', true); | ||
|
|
||
| expect(entity.hours).toBe('12'); | ||
| }); | ||
|
|
||
| it('should format hours 13-23 as 1-11 PM', () => { | ||
| const entity = new TimePickerEntity('14:45', true); | ||
|
|
||
| expect(entity.hours).toBe('02'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('24-hour format', () => { | ||
| it('should format hours with leading zero in 24-hour mode', () => { | ||
| const entity = new TimePickerEntity('9:30'); | ||
|
|
||
| expect(entity.hours).toBe('09'); | ||
| }); | ||
|
|
||
| it('should handle midnight in 24-hour format', () => { | ||
| const entity = new TimePickerEntity('0:00'); | ||
|
|
||
| expect(entity.hours).toBe('00'); | ||
| }); | ||
|
|
||
| it('should convert 12 AM to 00:00 in 24-hour format', () => { | ||
| const entity = new TimePickerEntity('12:00'); | ||
|
|
||
| expect(entity.hours).toBe('00'); | ||
ilyabrower marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }); | ||
|
|
||
| it('should convert 12 PM to 12:00 in 24-hour format', () => { | ||
| const entity = new TimePickerEntity('12:00'); | ||
| entity.toggleMeridiem(); | ||
|
|
||
| expect(entity.hours).toBe('12'); | ||
| }); | ||
|
|
||
| it('should convert PM hours correctly', () => { | ||
| const entity = new TimePickerEntity('3:00'); | ||
| entity.toggleMeridiem(); | ||
|
|
||
| expect(entity.hours).toBe('15'); | ||
| }); | ||
|
|
||
| it('should keep AM hours unchanged (except 12)', () => { | ||
| const entity = new TimePickerEntity('9:00'); | ||
|
|
||
| expect(entity.hours).toBe('09'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('toggleMeridiem', () => { | ||
| it('should toggle multiple times correctly', () => { | ||
| const entity = new TimePickerEntity('10:00', true); | ||
|
|
||
| entity.toggleMeridiem(); | ||
| expect(entity.meridiem).toBe('PM'); | ||
|
|
||
| entity.toggleMeridiem(); | ||
| expect(entity.meridiem).toBe('AM'); | ||
|
|
||
| entity.toggleMeridiem(); | ||
| expect(entity.meridiem).toBe('PM'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('toString', () => { | ||
| it('should return 24-hour format string when is12Hour is false', () => { | ||
| const entity = new TimePickerEntity('14:30'); | ||
|
|
||
| expect(entity.toString()).toBe('14:30'); | ||
| }); | ||
|
|
||
| it('should convert to 24-hour format string when is12Hour is true', () => { | ||
| const entity = new TimePickerEntity('2:30', true); | ||
| entity.toggleMeridiem(); | ||
|
|
||
| expect(entity.toString()).toBe('14:30'); | ||
| }); | ||
|
|
||
| it('should handle midnight (12 AM) conversion', () => { | ||
| const entity = new TimePickerEntity('12:00', true); | ||
|
|
||
| expect(entity.toString()).toBe('00:00'); | ||
| }); | ||
|
|
||
| it('should handle noon (12 PM) conversion', () => { | ||
| const entity = new TimePickerEntity('12:00', true); | ||
| entity.toggleMeridiem(); | ||
|
|
||
| expect(entity.toString()).toBe('12:00'); | ||
| }); | ||
|
|
||
| it('should add leading zeros to output', () => { | ||
| const entity = new TimePickerEntity('9:5'); | ||
|
|
||
| expect(entity.toString()).toBe('09:05'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('edge cases', () => { | ||
| it('should handle invalid hours gracefully', () => { | ||
| const entity = new TimePickerEntity('invalid:30'); | ||
|
|
||
| expect(entity.hours).toBe('invalid'); | ||
| }); | ||
|
|
||
| it('should handle empty minutes', () => { | ||
| const entity = new TimePickerEntity('10:'); | ||
|
|
||
| expect(entity.minutes).toBe(''); | ||
| }); | ||
|
|
||
| it('should handle empty hours', () => { | ||
| const entity = new TimePickerEntity(':30'); | ||
|
|
||
| expect(entity.hours).toBe(''); | ||
| }); | ||
|
|
||
| it('should preserve non-numeric hour values in 12-hour format', () => { | ||
| const entity = new TimePickerEntity('invalid:30', true); | ||
|
|
||
| expect(entity.hours).toBe('invalid'); | ||
| }); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,7 +9,7 @@ | |
| "author": "UI-kit team <[email protected]>", | ||
| "license": "MIT", | ||
| "scripts": { | ||
| "build": "pnpm semcore-builder --source=js && pnpm vite build" | ||
| "build": "pnpm semcore-builder && pnpm vite build" | ||
| }, | ||
| "exports": { | ||
| "require": "./lib/cjs/index.js", | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.