From c892ebba4ccecc633cbcaa64db197f3448440e68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Zaroda?= Date: Sun, 25 Aug 2024 23:46:52 +0200 Subject: [PATCH] Initial commit. --- .github/workflows/main.yml | 20 +++ .gitignore | 3 + LICENSE.txt | 19 +++ eslint.config.mjs | 24 ++++ jest.config.js | 8 ++ package.json | 41 ++++++ src/AbstractWraplet.ts | 165 +++++++++++++++++++++++ src/errors.ts | 3 + src/index.ts | 4 + src/types/Utils.ts | 11 ++ src/types/Wraplet.ts | 3 + src/types/WrapletChildDefinition.ts | 11 ++ src/types/WrapletChildren.ts | 10 ++ src/types/WrapletChildrenMap.ts | 5 + src/types/global.ts | 7 + tests/map.test.ts | 39 ++++++ tests/multiple-optional-children.test.ts | 54 ++++++++ tests/multiple-required-children.test.ts | 52 +++++++ tests/passing-arguments.test.ts | 97 +++++++++++++ tests/resources/BaseTestWraplet.ts | 39 ++++++ tests/setup.ts | 3 + tests/single-optional-child.test.ts | 55 ++++++++ tests/single-required-child.test.ts | 50 +++++++ tests/wraplet-initialization.test.ts | 59 ++++++++ tsconfig.build.json | 6 + tsconfig.json | 17 +++ webpack.config.js | 41 ++++++ 27 files changed, 846 insertions(+) create mode 100644 .github/workflows/main.yml create mode 100644 .gitignore create mode 100644 LICENSE.txt create mode 100644 eslint.config.mjs create mode 100644 jest.config.js create mode 100644 package.json create mode 100644 src/AbstractWraplet.ts create mode 100644 src/errors.ts create mode 100644 src/index.ts create mode 100644 src/types/Utils.ts create mode 100644 src/types/Wraplet.ts create mode 100644 src/types/WrapletChildDefinition.ts create mode 100644 src/types/WrapletChildren.ts create mode 100644 src/types/WrapletChildrenMap.ts create mode 100644 src/types/global.ts create mode 100644 tests/map.test.ts create mode 100644 tests/multiple-optional-children.test.ts create mode 100644 tests/multiple-required-children.test.ts create mode 100644 tests/passing-arguments.test.ts create mode 100644 tests/resources/BaseTestWraplet.ts create mode 100644 tests/setup.ts create mode 100644 tests/single-optional-child.test.ts create mode 100644 tests/single-required-child.test.ts create mode 100644 tests/wraplet-initialization.test.ts create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json create mode 100644 webpack.config.js diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..e0a3ef2 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,20 @@ +name: Tests +on: [push, pull_request, workflow_dispatch] +permissions: + contents: read + packages: read +jobs: + test: + name: Testing code. + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v3.5.3 + - uses: actions/setup-node@v4 + with: + node-version: 21 + - name: Installing dependencies. + run: yarn install + - name: Running tests. + run: yarn run tests + - name: Linting check. + run: yarn run lint diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e03f4f2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.idea/ +/*.iml +/yarn.lock \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..4c42f09 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2024-present Ɓukasz Zaroda + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..17de1e2 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,24 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; + +export default [ + {files: ["**/*.{js,mjs,cjs,ts}"]}, + {languageOptions: {globals: globals.browser}}, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + eslintPluginPrettierRecommended, + { + languageOptions: { + parserOptions: { + project: "./tsconfig.json", + } + }, + rules: { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-empty-object-type": "off", + "@typescript-eslint/prefer-as-const": "off", + }, + }, +]; \ No newline at end of file diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..6db1e85 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,8 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} **/ +module.exports = { + testEnvironment: "node", + transform: { + "^.+.tsx?$": ["ts-jest",{}], + }, + preset: "ts-jest", +}; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..04de30b --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "wraplet", + "version": "0.5.0", + "description": "", + "main": "dist/index.js", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/lukasz-zaroda/wraplet.git" + }, + "scripts": { + "setup": "yarn install", + "build": "./node_modules/.bin/webpack --mode production --config ./webpack.config.js", + "dev:build": "./node_modules/.bin/webpack --mode development --config ./webpack.config.js", + "dev:watch": "./node_modules/.bin/webpack --mode development --watch --config ./webpack.config.js", + "lint": "tsc --noemit && npx eslint './src/**/*.ts' './tests/**/*.ts'", + "lint:fix": "npx eslint './src/**/*.ts' './tests/**/*.ts' --fix", + "tests": "jest" + }, + "dependencies": { + }, + "devDependencies": { + "typescript": "^5.5.4", + "ts-loader": "^9.5.1", + "@eslint/js": "^9.9.1", + "eslint": "^9.9.1", + "globals": "^15.9.0", + "typescript-eslint": "^8.2.0", + "webpack": "^5.94.0", + "webpack-cli": "^5.1.4", + "prettier": "^3.3.3", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "prettier-eslint": "^16.3.0", + "jest": "^29.7.0", + "jest-cli": "^29.7.0", + "ts-jest": "^29.2.5", + "@types/jest": "^29.5.12", + "jest-environment-jsdom": "^29.7.0" + } +} diff --git a/src/AbstractWraplet.ts b/src/AbstractWraplet.ts new file mode 100644 index 0000000..822e8ef --- /dev/null +++ b/src/AbstractWraplet.ts @@ -0,0 +1,165 @@ +import { WrapletChildrenMap } from "./types/WrapletChildrenMap"; +import { WrapletChildren } from "./types/WrapletChildren"; +import { Wraplet } from "./types/Wraplet"; +import { DeepWriteable, Nullable } from "./types/Utils"; +import { MapError, MissingRequiredChildError } from "./errors"; + +export abstract class AbstractWraplet< + T extends WrapletChildrenMap = {}, + E extends Element = Element, +> implements Wraplet +{ + public isWraplet: true = true; + + protected children: WrapletChildren; + protected static debug: boolean = false; + + constructor( + protected element: E, + mapAlterCallback: ((map: DeepWriteable) => void) | null = null, + ) { + const map = this.buildChildrenMap(); + if (mapAlterCallback) { + mapAlterCallback(map); + } + this.children = this.instantiateChildren(map); + if (!this.element.wraplets) { + this.element.wraplets = []; + } + this.element.wraplets.push(this); + } + + protected abstract buildChildrenMap(): T; + + protected instantiateChildren(map: T): WrapletChildren { + const children: Partial>> = {}; + for (const id in map) { + const item = map[id]; + const selector = item.selector; + const wrapletClass = item.Class; + const args = item.args || []; + const multiple = item.multiple; + const isRequired = item.required; + + if (!selector) { + if (isRequired) { + throw new MapError( + `${this.constructor.name}: Child "${id}" cannot at the same be required and have no selector.`, + ); + } + + children[id] = multiple + ? ([] as WrapletChildren[keyof WrapletChildren]) + : null; + continue; + } + + const childElements = this.element.querySelectorAll(selector); + if (childElements.length === 0) { + if (isRequired) { + throw new MissingRequiredChildError( + `${this.constructor.name}: Couldn't find an element for the wraplet "${id}". Selector used: "${selector}".`, + ); + } + if ((this.constructor as any).debug) { + console.log( + `${this.constructor.name}: Optional child '${id}' has not been found. Selector used: "${selector}"`, + ); + } + children[id] = multiple + ? ([] as WrapletChildren[keyof WrapletChildren]) + : null; + + continue; + } + + const childWraplet: Wraplet[] = []; + for (const childElement of childElements) { + childWraplet.push(this.createWraplet(wrapletClass, childElement, args)); + if (!multiple) { + break; + } + } + + const value: Wraplet | Wraplet[] = multiple + ? childWraplet + : childWraplet[0]; + if (multiple && !value && (this.constructor as any).debug) { + console.log( + `${this.constructor.name}: no items for the multiple child '${id}' have been found. Selector used: "${selector}"`, + ); + } + if (!this.childTypeGuard(value, id)) { + if (typeof value === "undefined") { + throw new Error( + `${this.constructor.name}: Couldn't intantionate the "${id}" child. Selector used: "${selector}".`, + ); + } + throw new Error( + `${this.constructor.name}: Child value doesn't match the map. Value: ${value}. Expected: ${map[id]["Class"].name}`, + ); + } + + children[id] = value; + } + + // Now we should have all properties set, so let's assert the final form. + return children as WrapletChildren; + } + + protected createWraplet( + wrapletClass: new (...args: any[]) => Wraplet, + childElement: Element, + args: unknown[] = [], + ): Wraplet { + return new wrapletClass(...[...[childElement], ...args]); + } + + private childTypeGuard>( + variable: Wraplet | Wraplet[] | null, + id: S, + ): variable is WrapletChildren[S] { + const map = this.buildChildrenMap(); + const Class = map[id].Class; + const isRequired = map[id].required; + const isMultiple = map[id].multiple; + if (isMultiple) { + const isArray = Array.isArray(variable); + if (!isArray) { + return false; + } + if (isRequired) { + return variable.every((value) => value instanceof Class); + } + + return true; + } + + if (isRequired) { + return variable instanceof Class; + } + + return variable instanceof Class || variable === null; + } + + // We can afford "any" here because this method is only for the external usage, and external + // callers don't need to know what map is the current wraplet using, as it's its internal + // matter. + protected static createWraplets = never>( + document: Document, + additional_args: unknown[] = [], + selector: string, + ): T[] { + if (this instanceof AbstractWraplet) { + throw new Error("You cannot instantiate an abstract class."); + } + + const result: T[] = []; + const foundElements = document.querySelectorAll(selector); + for (const element of foundElements) { + result.push(new (this as any)(element, ...additional_args)); + } + + return result; + } +} diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..eeae956 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,3 @@ +export class MissingRequiredChildError extends Error {} + +export class MapError extends Error {} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..6f2e4c5 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,4 @@ +export { AbstractWraplet } from "./AbstractWraplet"; +export { WrapletChildrenMap } from "./types/WrapletChildrenMap"; + +import "./types/global"; diff --git a/src/types/Utils.ts b/src/types/Utils.ts new file mode 100644 index 0000000..95648fd --- /dev/null +++ b/src/types/Utils.ts @@ -0,0 +1,11 @@ +export type InstantiableReturnType = T extends { + new (...args: any[]): infer R; +} + ? R + : never; + +export type Nullable = { [K in keyof T]: T[K] | null }; + +export type DeepWriteable = { + -readonly [P in keyof T]: DeepWriteable; +}; diff --git a/src/types/Wraplet.ts b/src/types/Wraplet.ts new file mode 100644 index 0000000..10c8918 --- /dev/null +++ b/src/types/Wraplet.ts @@ -0,0 +1,3 @@ +export interface Wraplet { + isWraplet: true; +} diff --git a/src/types/WrapletChildDefinition.ts b/src/types/WrapletChildDefinition.ts new file mode 100644 index 0000000..1766a2a --- /dev/null +++ b/src/types/WrapletChildDefinition.ts @@ -0,0 +1,11 @@ +import { AbstractWraplet } from "../AbstractWraplet"; + +export type WrapletChildDefinition< + T extends AbstractWraplet = AbstractWraplet, +> = { + selector?: string; + Class: { new (...args: any[]): T }; + required: boolean; + multiple: boolean; + args?: unknown[]; +}; diff --git a/src/types/WrapletChildren.ts b/src/types/WrapletChildren.ts new file mode 100644 index 0000000..d3f69a9 --- /dev/null +++ b/src/types/WrapletChildren.ts @@ -0,0 +1,10 @@ +import { WrapletChildrenMap } from "./WrapletChildrenMap"; +import { InstantiableReturnType } from "./Utils"; + +export type WrapletChildren = { + [id in keyof T]: T[id]["multiple"] extends true + ? InstantiableReturnType[] + : T[id]["required"] extends true + ? InstantiableReturnType + : InstantiableReturnType | null; +}; diff --git a/src/types/WrapletChildrenMap.ts b/src/types/WrapletChildrenMap.ts new file mode 100644 index 0000000..2ca0c9e --- /dev/null +++ b/src/types/WrapletChildrenMap.ts @@ -0,0 +1,5 @@ +import { WrapletChildDefinition } from "./WrapletChildDefinition"; + +export type WrapletChildrenMap = { + [id: string]: WrapletChildDefinition; +}; diff --git a/src/types/global.ts b/src/types/global.ts new file mode 100644 index 0000000..c9f1197 --- /dev/null +++ b/src/types/global.ts @@ -0,0 +1,7 @@ +import { Wraplet } from "./Wraplet"; + +declare global { + interface Element { + wraplets?: Wraplet[]; + } +} diff --git a/tests/map.test.ts b/tests/map.test.ts new file mode 100644 index 0000000..4b315ad --- /dev/null +++ b/tests/map.test.ts @@ -0,0 +1,39 @@ +/** + * @jest-environment jsdom + */ +import "./setup"; +import { AbstractWraplet, WrapletChildrenMap } from "../src"; +import { BaseTestWraplet } from "./resources/BaseTestWraplet"; +import { MapError } from "../src/errors"; + +const testWrapletSelectorAttribute = "data-test-selector"; + +class TestWrapletChild extends AbstractWraplet { + protected buildChildrenMap(): {} { + return {}; + } +} + +const childrenMap = { + children: { + Class: TestWrapletChild, + multiple: false, + required: true, + }, +} as const satisfies WrapletChildrenMap; + +class TestWraplet extends BaseTestWraplet { + protected buildChildrenMap(): typeof childrenMap { + return childrenMap; + } +} + +// TESTS START HERE + +test("Test that `required` and missing selector are mutually exclusive", () => { + document.body.innerHTML = `
`; + const createWraplet = () => { + TestWraplet.create(testWrapletSelectorAttribute); + }; + expect(createWraplet).toThrowError(MapError); +}); diff --git a/tests/multiple-optional-children.test.ts b/tests/multiple-optional-children.test.ts new file mode 100644 index 0000000..a52e096 --- /dev/null +++ b/tests/multiple-optional-children.test.ts @@ -0,0 +1,54 @@ +/** + * @jest-environment jsdom + */ +import "./setup"; +import { AbstractWraplet, WrapletChildrenMap } from "../src"; +import { BaseTestWraplet } from "./resources/BaseTestWraplet"; + +const testWrapletSelectorAttribute = "data-test-selector"; +const testWrapletChildSelectorAttribute = `${testWrapletSelectorAttribute}-child`; + +class TestWrapletChild extends AbstractWraplet { + protected buildChildrenMap(): {} { + return {}; + } +} + +const childrenMap = { + children: { + selector: `[${testWrapletChildSelectorAttribute}]`, + Class: TestWrapletChild, + multiple: true, + required: false, + }, +} as const satisfies WrapletChildrenMap; + +class TestWraplet extends BaseTestWraplet { + protected buildChildrenMap(): typeof childrenMap { + return childrenMap; + } +} + +// TESTS START HERE + +test("Test wraplet optional children initialization empty children", () => { + document.body.innerHTML = `
`; + + const wraplet = TestWraplet.create(testWrapletSelectorAttribute); + if (!wraplet) { + throw new Error("Wraplet not initialized."); + } + const children = wraplet.getChild("children"); + expect(children).toHaveLength(0); +}); + +test("Test wraplet optional children initialization", () => { + document.body.innerHTML = `
`; + + const wraplet = TestWraplet.create(testWrapletSelectorAttribute); + if (!wraplet) { + throw new Error("Wraplet not initialized."); + } + const children = wraplet.getChild("children"); + expect(children).toHaveLength(2); +}); diff --git a/tests/multiple-required-children.test.ts b/tests/multiple-required-children.test.ts new file mode 100644 index 0000000..5c3bf45 --- /dev/null +++ b/tests/multiple-required-children.test.ts @@ -0,0 +1,52 @@ +/** + * @jest-environment jsdom + */ +import "./setup"; +import { MissingRequiredChildError } from "../src/errors"; +import { AbstractWraplet, WrapletChildrenMap } from "../src"; +import { BaseTestWraplet } from "./resources/BaseTestWraplet"; + +const testWrapletSelectorAttribute = "data-test-selector"; +const testWrapletChildSelectorAttribute = `${testWrapletSelectorAttribute}-child`; + +class TestWrapletChild extends AbstractWraplet { + protected buildChildrenMap(): {} { + return {}; + } +} + +const childrenMap = { + children: { + selector: `[${testWrapletChildSelectorAttribute}]`, + Class: TestWrapletChild, + multiple: true, + required: true, + }, +} as const satisfies WrapletChildrenMap; + +class TestWraplet extends BaseTestWraplet { + protected buildChildrenMap(): typeof childrenMap { + return childrenMap; + } +} + +// TESTS START HERE + +test("Test wraplet required children initialization empty children", () => { + document.body.innerHTML = `
`; + const getWraplet = () => { + TestWraplet.create(testWrapletSelectorAttribute); + }; + expect(getWraplet).toThrowError(MissingRequiredChildError); +}); + +test("Test wraplet required children initialization", () => { + document.body.innerHTML = `
`; + + const wraplet = TestWraplet.create(testWrapletSelectorAttribute); + if (!wraplet) { + throw new Error("Wraplet not initialized."); + } + const children = wraplet.getChild("children"); + expect(children).toHaveLength(2); +}); diff --git a/tests/passing-arguments.test.ts b/tests/passing-arguments.test.ts new file mode 100644 index 0000000..e7ba91e --- /dev/null +++ b/tests/passing-arguments.test.ts @@ -0,0 +1,97 @@ +/** + * @jest-environment jsdom + */ +import "./setup"; +import { AbstractWraplet, WrapletChildrenMap } from "../src"; +import { BaseTestWraplet } from "./resources/BaseTestWraplet"; +import { DeepWriteable } from "../src/types/Utils"; + +const testWrapletSelectorAttribute = "data-test-selector"; +const testWrapletChildSelectorAttribute = `${testWrapletSelectorAttribute}-child`; + +class TestWrapletChild extends AbstractWraplet { + private someString: string; + constructor(element: Element, stringArgument: string) { + super(element); + this.someString = stringArgument; + } + public getSomeString(): string { + return this.someString; + } + protected buildChildrenMap(): {} { + return {}; + } +} + +const childrenMap = { + child: { + selector: `[${testWrapletChildSelectorAttribute}]`, + Class: TestWrapletChild, + multiple: false, + required: false, + args: [] as unknown[], + }, +} as const satisfies WrapletChildrenMap; + +class TestWraplet extends BaseTestWraplet { + private readonly someString: string; + constructor(element: Element, stringArgument: string) { + const mapAlter = function (map: DeepWriteable) { + map["child"]["args"] = [stringArgument]; + }; + super(element, mapAlter); + this.someString = stringArgument; + } + + public getSomeString(): string { + return this.someString; + } + + protected buildChildrenMap(): typeof childrenMap { + return childrenMap; + } + + public static createWithArguments< + C extends BaseTestWraplet, + >(selectorAttribute: string, someString: string): C | null { + const wraplets = this.createWraplets( + document, + [someString], + `[${selectorAttribute}]`, + ); + if (wraplets.length === 0) { + return null; + } + + return wraplets[0]; + } +} + +// TESTS START HERE + +test("Test passing arguments", () => { + const str = "some string"; + document.body.innerHTML = `
`; + const wraplet = TestWraplet.createWithArguments( + testWrapletSelectorAttribute, + str, + ); + + expect(wraplet?.getSomeString()).toBe(str); +}); + +test("Test passing arguments to child", () => { + const str = "some string"; + document.body.innerHTML = `
`; + const wraplet = TestWraplet.createWithArguments( + testWrapletSelectorAttribute, + str, + ); + + const child = wraplet?.getChild("child"); + if (!child) { + throw new Error("Wraplet child not initialized."); + } + + expect(child.getSomeString()).toBe(str); +}); diff --git a/tests/resources/BaseTestWraplet.ts b/tests/resources/BaseTestWraplet.ts new file mode 100644 index 0000000..87c155a --- /dev/null +++ b/tests/resources/BaseTestWraplet.ts @@ -0,0 +1,39 @@ +import { AbstractWraplet, WrapletChildrenMap } from "../../src"; +import { WrapletChildren } from "../../src/types/WrapletChildren"; + +export abstract class BaseTestWraplet< + T extends WrapletChildrenMap, +> extends AbstractWraplet { + public addAttribute(name: string, value: string) { + this.element.setAttribute(name, value); + } + + public getChild(name: C): WrapletChildren[C] { + return this.children[name]; + } + + public hasChild(name: keyof T): boolean { + return !!this.children[name]; + } + + public static create>( + selectorAttribute: string, + ): C | null { + const wraplets = this.createWraplets( + document, + [], + `[${selectorAttribute}]`, + ); + if (wraplets.length === 0) { + return null; + } + + return wraplets[0]; + } + + public static createAll>( + selectorAttribute: string, + ): C[] { + return this.createWraplets(document, [], `[${selectorAttribute}]`) as C[]; + } +} diff --git a/tests/setup.ts b/tests/setup.ts new file mode 100644 index 0000000..4b21a2a --- /dev/null +++ b/tests/setup.ts @@ -0,0 +1,3 @@ +afterEach(() => { + document.getElementsByTagName("html")[0].innerHTML = ""; +}); diff --git a/tests/single-optional-child.test.ts b/tests/single-optional-child.test.ts new file mode 100644 index 0000000..168eaf2 --- /dev/null +++ b/tests/single-optional-child.test.ts @@ -0,0 +1,55 @@ +/** + * @jest-environment jsdom + */ + +import "./setup"; +import { AbstractWraplet, WrapletChildrenMap } from "../src"; +import { BaseTestWraplet } from "./resources/BaseTestWraplet"; + +const testWrapletSelectorAttribute = "data-test-selector"; +const testWrapletChildSelectorAttribute = `${testWrapletSelectorAttribute}-child`; + +class TestWrapletChild extends AbstractWraplet { + protected buildChildrenMap(): {} { + return {}; + } + + public hasElement(): boolean { + return !!this.element; + } +} + +const childrenMap = { + child: { + selector: `[${testWrapletChildSelectorAttribute}]`, + Class: TestWrapletChild, + multiple: false, + required: false, + }, +} as const satisfies WrapletChildrenMap; + +class TestWraplet extends BaseTestWraplet { + protected buildChildrenMap(): typeof childrenMap { + return childrenMap; + } +} + +// TESTS START HERE + +test("Test wraplet optional single child initialization", () => { + document.body.innerHTML = `
`; + const wraplet = TestWraplet.create(testWrapletSelectorAttribute); + if (!wraplet) { + throw Error("Wraplet not initialized."); + } + expect(wraplet.getChild("child")).toBeInstanceOf(TestWrapletChild); +}); + +test("Test wraplet child has element", () => { + document.body.innerHTML = `
`; + const wraplet = TestWraplet.create(testWrapletSelectorAttribute); + if (!wraplet) { + throw Error("Wraplet not initialized."); + } + expect(wraplet.getChild("child")?.hasElement()).toBeTruthy(); +}); diff --git a/tests/single-required-child.test.ts b/tests/single-required-child.test.ts new file mode 100644 index 0000000..056544c --- /dev/null +++ b/tests/single-required-child.test.ts @@ -0,0 +1,50 @@ +/** + * @jest-environment jsdom + */ +import "./setup"; +import { MissingRequiredChildError } from "../src/errors"; +import { AbstractWraplet, WrapletChildrenMap } from "../src"; +import { BaseTestWraplet } from "./resources/BaseTestWraplet"; + +const testWrapletSelectorAttribute = "data-test-selector"; +const testWrapletChildSelectorAttribute = `${testWrapletSelectorAttribute}-child`; + +class TestWrapletChild extends AbstractWraplet { + protected buildChildrenMap(): {} { + return {}; + } +} + +const childrenMap = { + child: { + selector: `[${testWrapletChildSelectorAttribute}]`, + Class: TestWrapletChild, + multiple: false, + required: true, + }, +} as const satisfies WrapletChildrenMap; + +class TestWraplet extends BaseTestWraplet { + protected buildChildrenMap(): typeof childrenMap { + return childrenMap; + } +} + +// TESTS START HERE + +test("Test wraplet single child required", () => { + document.body.innerHTML = `
`; + const wraplet = TestWraplet.create(testWrapletSelectorAttribute); + if (!wraplet) { + throw Error("Wraplet not initialized."); + } + expect(wraplet.getChild("child")).toBeInstanceOf(TestWrapletChild); +}); + +test("Test wraplet single child required failed", () => { + document.body.innerHTML = `
`; + const createWraplet = () => { + TestWraplet.create(testWrapletSelectorAttribute); + }; + expect(createWraplet).toThrowError(MissingRequiredChildError); +}); diff --git a/tests/wraplet-initialization.test.ts b/tests/wraplet-initialization.test.ts new file mode 100644 index 0000000..ca3a3eb --- /dev/null +++ b/tests/wraplet-initialization.test.ts @@ -0,0 +1,59 @@ +/** + * @jest-environment jsdom + */ +import "./setup"; +import { AbstractWraplet, WrapletChildrenMap } from "../src"; +import { BaseTestWraplet } from "./resources/BaseTestWraplet"; + +const testWrapletSelectorAttribute = "data-test-selector"; +const testWrapletChildSelectorAttribute = `${testWrapletSelectorAttribute}-child`; + +class TestWrapletChild extends AbstractWraplet { + protected buildChildrenMap(): {} { + return {}; + } +} + +const childrenMap = { + child: { + selector: `[${testWrapletChildSelectorAttribute}]`, + Class: TestWrapletChild, + multiple: false, + required: false, + }, +} as const satisfies WrapletChildrenMap; + +class TestWraplet extends BaseTestWraplet { + protected buildChildrenMap(): typeof childrenMap { + return childrenMap; + } + + public hasElement(): boolean { + return !!this.element; + } +} + +// TESTS START HERE + +test("Test wraplet initialization", () => { + document.body.innerHTML = `
`; + const wraplet = TestWraplet.create(testWrapletSelectorAttribute); + expect(wraplet).toBeTruthy(); +}); + +test("Test multiple wraplets initialization", () => { + document.body.innerHTML = `
`; + const wraplets = TestWraplet.createAll( + testWrapletSelectorAttribute, + ); + expect(wraplets.length).toEqual(2); +}); + +test("Test wraplet has element", () => { + document.body.innerHTML = `
`; + const wraplet = TestWraplet.create(testWrapletSelectorAttribute); + if (!wraplet) { + throw Error("Wraplet not initialized."); + } + expect(wraplet.hasElement()).toBeTruthy(); +}); diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..cae154c --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "tests" + ] +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1751b5d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "outDir": "./dist/", + "noImplicitAny": true, + "module": "es6", + "target": "es2017", + "jsx": "react", + "allowJs": true, + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "declaration": true, + }, + "include": [ + "src", "tests" + ], +} diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..5f461bb --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,41 @@ +const webpack = require('webpack'); +const path = require('path'); + +const devMode = process.env.NODE_ENV !== 'production'; + +module.exports = { + devtool: 'source-map', + entry: './src/index.ts', + resolve: { + extensions: ['.tsx', '.ts', '.js'], + }, + resolveLoader: { + modules: ['node_modules', path.resolve(__dirname, 'loaders')], + }, + output: { + filename: 'index.js', + path: path.resolve(__dirname, 'dist'), + clean: true, + library: { + type: 'umd', + name: 'wraplet', + }, + }, + module: { + rules: [ + { + test: /\.tsx?$/, + loader: 'ts-loader', + exclude: '/node_modules/', + options: { + configFile: "tsconfig.build.json" + } + }, + ], + }, + plugins: [ + new webpack.DefinePlugin({ + BASEPATH: JSON.stringify(''), + }), + ] +};