Add tests for declaform.

This commit is contained in:
Austin Smith
2025-10-19 11:06:46 -04:00
parent ed00ede04f
commit a3c34de3d2
6 changed files with 4944 additions and 8 deletions

11
jest.config.js Normal file
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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"
} }
} }

View File

@@ -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);

View 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', '');
}

View File

@@ -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"]
} }