Add tests for declaform.
This commit is contained in:
11
jest.config.js
Normal file
11
jest.config.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
const { createDefaultPreset } = require("ts-jest");
|
||||||
|
|
||||||
|
const tsJestTransformCfg = createDefaultPreset().transform;
|
||||||
|
|
||||||
|
/** @type {import("jest").Config} **/
|
||||||
|
module.exports = {
|
||||||
|
testEnvironment: "jsdom",
|
||||||
|
transform: {
|
||||||
|
...tsJestTransformCfg,
|
||||||
|
},
|
||||||
|
};
|
||||||
4775
package-lock.json
generated
4775
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,11 +4,15 @@
|
|||||||
"description": "A collection of useful web components and functionality.",
|
"description": "A collection of useful web components and functionality.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "npx jest"
|
||||||
},
|
},
|
||||||
"author": "Austin Smith",
|
"author": "Austin Smith",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/jest": "^30.0.0",
|
||||||
|
"jest": "^30.2.0",
|
||||||
|
"jest-environment-jsdom": "^30.2.0",
|
||||||
|
"ts-jest": "^29.4.5",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class Field {
|
|||||||
return this._name;
|
return this._name;
|
||||||
}
|
}
|
||||||
|
|
||||||
get value() {
|
get value(): string | string[] | undefined {
|
||||||
switch(this._type) {
|
switch(this._type) {
|
||||||
case 'checkbox':
|
case 'checkbox':
|
||||||
return (this._inputs as HTMLInputElement[]).filter(x => x.checked).map(x => x.value);
|
return (this._inputs as HTMLInputElement[]).filter(x => x.checked).map(x => x.value);
|
||||||
@@ -54,7 +54,30 @@ class Field {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
set value(x: any) {
|
set value(val: string) {
|
||||||
|
switch(this._type) {
|
||||||
|
case 'radio':
|
||||||
|
case 'checkbox':
|
||||||
|
(this._inputs as HTMLInputElement[])
|
||||||
|
.forEach(input => {
|
||||||
|
if (input.value === val) {
|
||||||
|
input.setAttribute('checked', '');
|
||||||
|
} else {
|
||||||
|
input.removeAttribute('checked');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
case 'date':
|
||||||
|
case 'datatime-local':
|
||||||
|
case 'email':
|
||||||
|
case 'number':
|
||||||
|
case 'password':
|
||||||
|
case 'select':
|
||||||
|
case 'text':
|
||||||
|
this._inputs.forEach(x => x.value = val);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get type() {
|
get type() {
|
||||||
@@ -121,10 +144,12 @@ export class Declaform extends HTMLElement {
|
|||||||
for (const [name, inputs] of inputGroups.entries()) {
|
for (const [name, inputs] of inputGroups.entries()) {
|
||||||
this._fields.push(new Field(name, inputs))
|
this._fields.push(new Field(name, inputs))
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
const src = this.getAttribute('src');
|
const src = this.getAttribute('src');
|
||||||
if (src) {
|
if (src) {
|
||||||
this.hydrateForm(src);
|
this.hydrateForm(src);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
get form() {
|
get form() {
|
||||||
@@ -135,7 +160,7 @@ export class Declaform extends HTMLElement {
|
|||||||
return this._fields;
|
return this._fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toObject() {
|
||||||
const setProperty = (obj: Record<string, any>, name: string, value: any) => {
|
const setProperty = (obj: Record<string, any>, name: string, value: any) => {
|
||||||
const [current, children] = name.split('.');
|
const [current, children] = name.split('.');
|
||||||
if (!current) {
|
if (!current) {
|
||||||
@@ -156,12 +181,11 @@ export class Declaform extends HTMLElement {
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async hydrateForm(endpoint: string) {
|
hydrate(obj: Record<string, any>) {
|
||||||
const response = await fetch(endpoint);
|
|
||||||
const obj = await response.json();
|
|
||||||
const populateFields = (current: Record<string, any>, path: string) => {
|
const populateFields = (current: Record<string, any>, path: string) => {
|
||||||
for (const key of Object.keys(current)) {
|
for (const key of Object.keys(current)) {
|
||||||
const value = current[key];
|
const value = current[key];
|
||||||
|
console.log(`Visiting ${key} with value ${value}`)
|
||||||
const propPath = (path.length) ? `${path}.${key}` : key;
|
const propPath = (path.length) ? `${path}.${key}` : key;
|
||||||
if (typeof value !== 'object') {
|
if (typeof value !== 'object') {
|
||||||
const fields = this.fields.filter(x => x.name === propPath);
|
const fields = this.fields.filter(x => x.name === propPath);
|
||||||
|
|||||||
120
src/declaform/tests/declaform.test.ts
Normal file
120
src/declaform/tests/declaform.test.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
//import '../declaform';
|
||||||
|
import { Declaform } from '../declaform';
|
||||||
|
|
||||||
|
const MOVIE_FORM = `
|
||||||
|
<decla-form id="form">
|
||||||
|
<form slot="form">
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input id="name" name="name" />
|
||||||
|
</div>
|
||||||
|
<select id="genre" name="genre">
|
||||||
|
<option value="comedy">Comedy</option>
|
||||||
|
<option value="horror">Horror</option>
|
||||||
|
</select>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Rating</legend>
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="rating-type-user">Type</label>
|
||||||
|
<input id="rating-type-user" name="rating.type" type="radio" value="user" />
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="rating-type-critic">Type</label>
|
||||||
|
<input id="rating-type-critic" name="rating.type" type="radio" value="critic" />
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="rating-score">Score</label>
|
||||||
|
<input id="rating-score" name="rating.score" type="number" />
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
</decla-form>
|
||||||
|
`;
|
||||||
|
|
||||||
|
describe('Declaform', () => {
|
||||||
|
it('Should mount.', () => {
|
||||||
|
document.body.innerHTML = MOVIE_FORM;
|
||||||
|
expect(document.getElementById('form')).toBeTruthy();
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.toObject() should serialize form into a JS object', () => {
|
||||||
|
document.body.innerHTML = MOVIE_FORM;
|
||||||
|
const form = document.getElementById('form') as Declaform;
|
||||||
|
setInputValue('name', 'Monty Python');
|
||||||
|
setInputValue('genre', 'comedy');
|
||||||
|
setRadioValue('rating.type', 'user');
|
||||||
|
setInputValue('rating-score', '5');
|
||||||
|
const obj = form.toObject();
|
||||||
|
expect(obj).toMatchObject({
|
||||||
|
name: 'Monty Python',
|
||||||
|
genre: 'comedy',
|
||||||
|
rating: {
|
||||||
|
score: 5,
|
||||||
|
type: 'user',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('.hydrate() should populate input fields from a JS object', () => {
|
||||||
|
document.body.innerHTML = MOVIE_FORM;
|
||||||
|
const form = document.getElementById('form') as Declaform;
|
||||||
|
form.hydrate({
|
||||||
|
name: 'Jaws',
|
||||||
|
genre: 'horror',
|
||||||
|
rating: {
|
||||||
|
score: 5,
|
||||||
|
type: 'critic',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const obj = form.toObject();
|
||||||
|
expect(obj).toMatchObject({
|
||||||
|
name: 'Jaws',
|
||||||
|
genre: 'horror',
|
||||||
|
rating: {
|
||||||
|
score: 5,
|
||||||
|
type: 'critic',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
function setInputValue(id: string, value: string) {
|
||||||
|
const input = document.getElementById(id);
|
||||||
|
if (!input) {
|
||||||
|
throw new Error(`No input with id "${id}"!`);
|
||||||
|
}
|
||||||
|
if (!('value' in input)) {
|
||||||
|
throw new Error(`Tag with id "${id}" does not have "value" attribute!`);
|
||||||
|
}
|
||||||
|
input.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInputValue(id: string) {
|
||||||
|
const input = document.getElementById(id);
|
||||||
|
if (!input) {
|
||||||
|
throw new Error(`No input with id "${id}"!`);
|
||||||
|
}
|
||||||
|
if (!('value' in input)) {
|
||||||
|
throw new Error(`Tag with id "${id}" does not have "value" attribute!`);
|
||||||
|
}
|
||||||
|
return input.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRadioValue(name: string, value: string) {
|
||||||
|
const radios = document.querySelectorAll(`[name="${name}"]`);
|
||||||
|
if (!radios) {
|
||||||
|
throw new Error(`No radios with name "${name}"!`);
|
||||||
|
}
|
||||||
|
radios.forEach(radio => {
|
||||||
|
if (!('value' in radio)) {
|
||||||
|
throw new Error('Radio option does not have "value" attribute!');
|
||||||
|
}
|
||||||
|
radio.removeAttribute('checked');
|
||||||
|
})
|
||||||
|
const option = ([...radios] as HTMLInputElement[]).find(x => x.value);
|
||||||
|
if (!option) {
|
||||||
|
throw new Error(`No option with value "${value}"`);
|
||||||
|
}
|
||||||
|
option.setAttribute('checked', '');
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
// Environment Settings
|
// Environment Settings
|
||||||
// See also https://aka.ms/tsconfig/module
|
// See also https://aka.ms/tsconfig/module
|
||||||
|
"esModuleInterop": true,
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"target": "es6",
|
"target": "es6",
|
||||||
"types": [],
|
"types": [],
|
||||||
@@ -40,5 +41,6 @@
|
|||||||
"noUncheckedSideEffectImports": true,
|
"noUncheckedSideEffectImports": true,
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
}
|
},
|
||||||
|
"exclude": ["./src/**/*.test.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user