Skip to content

Commit

Permalink
✨ utils for element interact #51
Browse files Browse the repository at this point in the history
  • Loading branch information
trydofor committed Oct 11, 2024
1 parent 32a7ec0 commit 11910dd
Show file tree
Hide file tree
Showing 9 changed files with 301 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .changeset/light-bags-film.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@fessional/razor-common": patch
"@fessional/razor-mobile": patch
---

✨ utils for element interact #51
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"files.encoding": "utf8bom",
"files.autoGuessEncoding": true,
"cSpell.words": [
"composables",
"devs",
"dropoff",
"fessional",
Expand Down
40 changes: 40 additions & 0 deletions layers/common/test/DpiImg.vue.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { mount } from '@vue/test-utils';
import { describe, it, expect } from 'vitest';
import DpiImg from '../components/DpiImg.vue';

describe('DpiImg', () => {
it('renders the correct src and srcset attributes', () => {
const props = {
alt: 'Sample image',
srcset: ['image1.jpg', 'image2.jpg', 'image3.jpg'],
};

const wrapper = mount(DpiImg, {
props,
});

const img = wrapper.find('img');
expect(img.exists()).toBe(true);

expect(img.attributes('src')).toBe('image1.jpg');

const expectedSrcset = 'image1.jpg 1x, image2.jpg 2x, image3.jpg 3x';
expect(img.attributes('srcset')).toBe(expectedSrcset);

expect(img.attributes('alt')).toBe(props.alt);
});

it('renders with default alt when none is provided', () => {
const props = {
srcset: ['image1.jpg', 'image2.jpg'],
};

const wrapper = mount(DpiImg, {
props,
});

const img = wrapper.find('img');

expect(img.attributes('alt')).toBe(undefined);
});
});
69 changes: 69 additions & 0 deletions layers/common/test/vue-element.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { describe, it, expect, vi } from 'vitest';
import { ref, nextTick } from 'vue';
import { selectElement, focusElement, scrollElement } from '../utils/vue-element';

describe('selectElement', () => {
it('should return the element by id', () => {
const mockElement = document.createElement('div');
mockElement.id = 'test-element';
document.body.appendChild(mockElement);

const result = selectElement('test-element');
expect(result).toBe(mockElement);

// Clean up
document.body.removeChild(mockElement);
});

it('should return the element from Ref', () => {
const mockElement = document.createElement('div');
const mockRef = ref({ $el: mockElement });

const result = selectElement(mockRef);
expect(result).toBe(mockElement);
});
});

describe('focusElement', () => {
it('should call setFocus if the method exists', async () => {
const mockElement = {
setFocus: vi.fn(),
scrollIntoView: vi.fn(),
};
const mockRef = ref({ $el: mockElement });

await focusElement(mockRef);
await nextTick(); // wait for nextTick

expect(mockElement.setFocus).toHaveBeenCalled();
expect(mockElement.scrollIntoView).toHaveBeenCalledWith({ behavior: 'smooth', block: 'center' });
});

it('should call focus if setFocus does not exist', async () => {
const mockElement = {
focus: vi.fn(),
scrollIntoView: vi.fn(),
};
const mockRef = ref({ $el: mockElement });

await focusElement(mockRef);
await nextTick(); // wait for nextTick

expect(mockElement.focus).toHaveBeenCalled();
expect(mockElement.scrollIntoView).toHaveBeenCalledWith({ behavior: 'smooth', block: 'center' });
});
});

describe('scrollElement', () => {
it('should call scrollIntoView', async () => {
const mockElement = {
scrollIntoView: vi.fn(),
};
const mockRef = ref({ $el: mockElement });

await scrollElement(mockRef);
await nextTick(); // wait for nextTick

expect(mockElement.scrollIntoView).toHaveBeenCalledWith({ behavior: 'smooth', block: 'center' });
});
});
44 changes: 44 additions & 0 deletions layers/common/utils/vue-element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* select element by its ref or id
*
* @param element the element Ref or id
* @returns the element
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function selectElement(element: Ref<any> | string): HTMLElement & { setFocus?(): void } {
return typeof element === 'string'
? document.getElementById(element)
: element.value.$el;
}

/**
* focus the element and scroll it to center, support setFocus() method
* @param element the element Ref or id
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function focusElement(element: Ref<any> | string) {
nextTick(() => {
const ele = selectElement(element);

if (typeof ele.setFocus === 'function') {
ele.setFocus();
}
else {
ele.focus();
}

ele.scrollIntoView({ behavior: 'smooth', block: 'center' });
});
}

/**
* scroll the element to center
* @param element the element Ref or id
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function scrollElement(element: Ref<any> | string) {
nextTick(() => {
const ele = selectElement(element);
ele.scrollIntoView({ behavior: 'smooth', block: 'center' });
});
}
5 changes: 5 additions & 0 deletions layers/common/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ export default defineVitestConfig({
coverage: {
reporter: ['text', 'json-summary', 'json'],
reportOnFailure: true,
include: [
'**/components/**',
'**/composables/**',
'**/utils/**',
],
},
},
});
80 changes: 80 additions & 0 deletions layers/mobile/test/ionic-validator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Import testing utilities
import { describe, it, expect, vi } from 'vitest';
import { ref } from 'vue';
import { validateIonicInput } from '../utils/ionic-validator';

// Mock Ionic IonInput component
const mockClassList = {
add: vi.fn(),
remove: vi.fn(),
};

const mockInputRef = ref({
$el: {
classList: mockClassList,
},
});

describe('validateIonicInput', () => {
// Test case: valid input using regex pattern
it('should add ion-valid class for valid input', () => {
const validateFn = validateIonicInput(mockInputRef, /^[1-9][0-9]?$/);

// Simulate valid input
const inputEvent = { target: { value: '10' } } as unknown as Event;
const result = validateFn(inputEvent);

expect(result).toBe(true);
expect(mockClassList.add).toHaveBeenCalledWith('ion-valid');
expect(mockClassList.remove).toHaveBeenCalledWith('ion-invalid');
});

// Test case: invalid input using regex pattern
it('should add ion-invalid class for invalid input', () => {
const validateFn = validateIonicInput(mockInputRef, /^[1-9][0-9]?$/);

// Simulate invalid input
const inputEvent = { target: { value: '100' } } as unknown as Event;
const result = validateFn(inputEvent);

expect(result).toBe(false);
expect(mockClassList.add).toHaveBeenCalledWith('ion-invalid');
expect(mockClassList.remove).toHaveBeenCalledWith('ion-valid');
});

// Test case: input blur event
it('should add ion-touched class on blur', () => {
const validateFn = validateIonicInput(mockInputRef, /^[1-9][0-9]?$/);

// Simulate blur event
const blurEvent = new Event('blur');
const result = validateFn(blurEvent);

expect(result).toBeNull();
expect(mockClassList.add).toHaveBeenCalledWith('ion-touched');
});

// Test case: direct string input
it('should add ion-valid class for valid string input', () => {
const validateFn = validateIonicInput(mockInputRef, /^[1-9][0-9]?$/);

// Simulate valid string input
const result = validateFn('5');

expect(result).toBe(true);
expect(mockClassList.add).toHaveBeenCalledWith('ion-valid');
expect(mockClassList.remove).toHaveBeenCalledWith('ion-invalid');
});

// Test case: invalid string input
it('should add ion-invalid class for invalid string input', () => {
const validateFn = validateIonicInput(mockInputRef, /^[1-9][0-9]?$/);

// Simulate invalid string input
const result = validateFn('100');

expect(result).toBe(false);
expect(mockClassList.add).toHaveBeenCalledWith('ion-invalid');
expect(mockClassList.remove).toHaveBeenCalledWith('ion-valid');
});
});
51 changes: 51 additions & 0 deletions layers/mobile/utils/ionic-validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* ```tsx
* <template>
* <IonInput
* ref="pkgInputRef"
* v-model="pkgInput"
* type="number"
* error-text="need a positive number"
* @ion-input="onPkgInput"
* @ion-blur="onPkgInput"
* />
* </template>
* <script setup lang="ts">
* const pkgInputRef = ref();
* const onPkgInput = validateIonicInput(pkgInputRef, /^[1-9][0-9]?$/);
* </script>
* ```
*
* generate a validator function for ionic input.
* @param inputRef input ref
* @param checkFun check function or regex
*/
export function validateIonicInput(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
inputRef: Ref<any>,
checkFun: RegExp | ((value: string, event?: Event) => boolean),
): (ev: Event | string) => boolean | null {
return (ev: Event | string) => {
const classList = inputRef.value.$el.classList;
const isValue = typeof ev === 'string';

if (isValue || /blur/i.test(ev.type)) {
classList.add('ion-touched');
if (!isValue) return null;
}

const value = isValue ? ev : (ev.target as HTMLInputElement).value;
const valid = typeof checkFun === 'function' ? checkFun(value, isValue ? undefined : ev) : checkFun.test(value);

if (valid) {
classList.add('ion-valid');
classList.remove('ion-invalid');
}
else {
classList.remove('ion-valid');
classList.add('ion-invalid');
}

return valid;
};
}
5 changes: 5 additions & 0 deletions layers/mobile/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ export default defineVitestConfig({
coverage: {
reporter: ['text', 'json-summary', 'json'],
reportOnFailure: true,
include: [
'**/components/**',
'**/composables/**',
'**/utils/**',
],
},
},
});

0 comments on commit 11910dd

Please sign in to comment.