Compare commits

...

10 Commits

16 changed files with 605 additions and 254 deletions

20
README.md Normal file
View File

@ -0,0 +1,20 @@
# Prueba técnica de CGI
## Detalles de implementación
Se ha usado 100% VanillaJS, con una estructura similar a React mediante Web Components
## Start
Se puede usar cualquier web server para servir el contenido, no hay que compilar ni transpilar.
El script `start.sh` inicia mediante `npx` un servidor web simple en el directorio actual.
El contenido será servido en [http://localhost:8080](http://localhost:8080) por defecto.
## Tests
Se ha creado una test suite básica en VanillaJS
Para ejecutar los tests, entrar en [http://localhost:8080/test](http://localhost:8080/test) con el servidor en marcha.

45
SPEC.md Normal file
View File

@ -0,0 +1,45 @@
# Front-end
Se requiere maquetar (html y estilos) y desarrollar (javascript) una
aplicación para gestionar una lista de cadenas de texto.
Puedes encontrar el diseño en el siguiente enlace:
[https://xd.adobe.com/view/ea696dd0-8781-4460-8720-36deb2d19b2a-bf3a/](https://xd.adobe.com/view/ea696dd0-8781-4460-8720-36deb2d19b2a-bf3a/)
## Especificación Funcional
La aplicación debe tener una interfaz de usuario que cuente, al menos,
con los siguientes elementos:
* Un contenedor donde se irán añadiendo o borrando cadenas de
texto.
* Una caja de entrada de texto, donde el usuario pueda escribir los
textos que desee añadir a la lista.
* Un botón para agregar nuevas entradas.
* Un botón para eliminar de la lista.
La aplicación debe:
* Añadir entradas de texto, permitir al usuario escribir y añadir la
entrada de texto, a un listado. No se pueden añadir entradas
vacías.
* Eliminar un elemento de la lista (los ítems de la lista deben ser
seleccionables por el usuario). No se pueden eliminar elementos
de la lista sin haber seleccionado uno o varios de los elementos de
la lista. No se requiere poder seleccionar múltiples items para
poder borrarlos a la vez, pero se tendrá en cuenta. Es deseable,
pero no requerido, que el usuario pueda eliminar elementos de la
lista haciendo doble click sobre el ítem que se desea eliminar.
* Es deseable, pero no requerido, permitir al usuario deshacer, como
mínimo, el último cambio realizado. Para ello se deberá incluir un
botón de deshacer.
## Especificación Técnica.
Es deseable realizar una prueba en Vanilla JS y otra utilizando React. Si
se encuentran dificultades para realizar el desarrollo en Vanilla JS, se
puede entregar solo la prueba con React.

View File

@ -8,23 +8,7 @@
<link rel="icon" href="./favicon.ico" type="image/x-icon">
</head>
<body>
<script type="module" src="src/components/app-base.js" defer></script>
<script type="module" src="src/components/main-window.js" defer></script>
<script type="module" src="src/components/custom-button.js" defer></script>
<script type="module" src="src/components/item-list.js" defer></script>
<style>
html {
height: 100%;
}
body {
background: transparent linear-gradient(135deg, #A1C4FD 0%, #C2E9FB 100%) 0% 0% no-repeat padding-box;
opacity: 1;
display: flex;
justify-content: center;
align-items: center;
min-height: 100%;
}
</style>
<script type="module" src="src/app.js" defer></script>
<app-base></app-base>
</body>
</html>

109
src/app.js Normal file
View File

@ -0,0 +1,109 @@
import './components/main-window.js';
import './components/custom-button.js';
import './components/item-list.js';
import './components/custom-item.js';
import './components/custom-modal.js';
import './components/custom-text.js';
import Store from './modules/store.js';
import text from './modules/text.js';
class AppBase extends HTMLElement {
constructor() {
super();
this.text = text;
this.properties = {
color: '#324BFF',
};
this.store = new Store();
}
connectedCallback() {
this.store.subscribe(this.render.bind(this));
this.render();
}
render(action = 'INIT') {
if (![Store.actions.OPEN_MODAL, Store.actions.CLOSE_MODAL, Store.actions.ADD_LINE, Store.actions.REMOVE_LINE, Store.actions.UNDO, 'INIT'].includes(action)) {
return; // Only render if content changed
}
this.innerHTML = `
<style>
* {
margin: 0;
}
html {
height: 100%;
}
body {
background: transparent linear-gradient(135deg, #A1C4FD 0%, #C2E9FB 100%) 0% 0% no-repeat padding-box;
opacity: 1;
display: flex;
justify-content: center;
align-items: center;
min-height: 100%;
font-family: "Montserrat", sans-serif;
font-weight: normal;
font-size: 18px;
letter-spacing: 0px;
}
#title {
height: 49px;
margin-bottom: -12px;
text-align: center;
font-size: 40px;
font-weight: inherit;
color: #333333;
opacity: 1;
}
#description {
height: 74px;
text-align: center;
font-size: 18px;
color: #333333;
opacity: 1;
}
.button-bar {
display: flex;
column-gap: 35px
}
.button-bar > .right-button {
margin-left: auto;
}
#undo-char {
margin-top: 3px;
}
</style>
<main-window width="900px" gap="35px">
<h1 id="title">${this.text.title}</h1>
<p id="description">${this.text.description}</p>
<select id="list" multiple is="item-list" action="${Store.actions.SELECT}" height="227px">
${this.store.getRecords().map((item) => `<option is="custom-item" color="${this.properties.color}" value=${item.id}>${item.text}</option>`)}
</select>
<span class="button-bar">
<button is="custom-button" action="${Store.actions.UNDO}" outline width="81px" color="${this.properties.color}">
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" id="undo-char" width="21" height="19" viewBox="0 0 5.556 5.027">
<path d="M.681 2.22a2.38 2.38 0 0 1 4.742.294v0a2.38 2.38 0 0 1-4.526 1.03" style="fill:none;stroke:${this.properties.color};stroke-width:.264583;stroke-linecap:round;stroke-dasharray:none"/>
<path d="m.132 1.525.437.964.964-.437" style="fill:none;stroke:${this.properties.color};stroke-width:.264583;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"/>
</svg>
</button>
<button is="custom-button" action="${Store.actions.REMOVE_LINE}" outline width="124px" color="${this.properties.color}">Delete</button>
<button is="custom-button" class="right-button" action="${Store.actions.OPEN_MODAL}" width="138px" color="${this.properties.color}">Add</button>
</span>
</main-window>
<custom-modal active="${this.store.state.modalActive}">
<p>${text.modal.description}</p>
<input is="custom-text" placeholder="${this.text.modal.placeholder}"/>
<span class="button-bar">
<button is="custom-button" class="right-button" action="${Store.actions.ADD_LINE}" width="138px" color="${this.properties.color}">Add</button>
<button is="custom-button" action="${Store.actions.CLOSE_MODAL}" outline width="124px" color="${this.properties.color}">Cancel</button>
</span>
</custom-modal>
`;
}
}
customElements.define('app-base', AppBase);

View File

@ -1,43 +0,0 @@
import Store from '../modules/store.js';
import text from '../modules/text.js';
class AppBase extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.text = text;
this.store = new Store();
}
connectedCallback() {
this.store.subscribe(this.render.bind(this));
this.render();
}
renderText() {
const textList = this.store.getRecords();
return textList.map((item) => `<option value=${item.id}>${item.text}</option>`).join('\n');
}
render(action = 'INITIAL') {
if (![Store.actions.ADD_LINE, Store.actions.REMOVE_LINE, Store.actions.UNDO, 'INITIAL'].includes(action)) {
return; // Only render if content changed
}
this.shadowRoot.innerHTML = `
<main-window title="${this.text.title}" description="${this.text.description}">
<select multiple is="item-list">
${this.renderText()}
</select>
<span id="button-bar">
<custom-button action="${Store.actions.REMOVE_LINE}" color="#324bff">Delete</custom-button>
<custom-button action="${Store.actions.ADD_LINE}" color="#324bff">Add</custom-button>
<custom-button action="${Store.actions.UNDO}" outline color="#324bff">Undo</custom-button>
</span>
</main-window>
`;
}
}
customElements.define('app-base', AppBase);

View File

@ -1,58 +1,66 @@
import Store from '../modules/store.js';
class CustomButton extends HTMLElement {
static observedAttributes = ['outline', 'color', 'action'];
class CustomButton extends HTMLButtonElement {
static observedAttributes = ['outline', 'color', 'width'];
constructor() {
super();
this.publish = Store.publish;
this.attachShadow({ mode: 'open' });
}
onClick(e) {
this.publish(this.getAttribute('action'));
}
connectedCallback() {
this.render();
this.shadowRoot.getElementById('button').addEventListener('click', this.onClick.bind(this));
this.updateStyle();
this.addEventListener('click', this.onClick.bind(this));
}
disconnectedCallback() {
this.shadowRoot.getElementById('button').removeEventListener('click', this.onClick);
this.removeEventListener('click', this.onClick);
}
attributeChangedCallback() {
this.render();
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) return;
let isOutline;
let color;
switch (name) {
case 'outline':
isOutline = (newValue !== null && newValue !== 'false')
color = this.getAttribute('color');
this.style.backgroundColor = isOutline ? 'white' : color;
this.style.color = isOutline ? color : 'white';
break;
case 'color':
isOutline = this.getAttribute('outline') !== null && this.getAttribute('outline') !== 'false';
this.style.backgroundColor = isOutline ? 'white' : newValue;
this.style.color = isOutline ? newValue : 'white';
this.style.borderColor = newValue;
break;
case 'width':
this.style.width = newValue;
break;
default:
console.error(`Unhandled attribute change: ${name}`);
}
}
render() {
const isOutline = this.getAttribute('outline') !== null && this.getAttribute('outline') !== 'false';
this.shadowRoot.innerHTML = `
<style>
button {
border-radius: 50px;
border-width: 1px;
border-style: ${isOutline ? 'solid' : 'hidden'};
border-color: ${this.getAttribute('color')};
opacity: 1;
width: 138px;
height: 49px;
background-color: ${isOutline ? 'white' : this.getAttribute('color')};
color: ${isOutline ? this.getAttribute('color') : 'white'};
text-align: center;
font-size: 16px;
font-family: "Montserrat", sans-serif;
font-weight: normal
letter-spacing: 0px;
text-transform: uppercase;
}
</style>
<button id="button">
<slot></slot>
</button>
`;
updateStyle() {
this.style.borderRadius = '50px';
this.style.borderWidth = '1px';
this.style.borderStyle = 'solid';
this.style.opacity = '1';
this.style.textAlign = 'center';
this.style.fontSize = '16px';
this.style.fontFamily = '"Montserrat", sans-serif';
this.style.fontWeight = 'normal';
this.style.letterSpacing = '0px';
this.style.textTransform = 'uppercase';
this.style.height = '49px';
}
onClick() {
this.publish(this.getAttribute('action'));
}
}
customElements.define('custom-button', CustomButton);
customElements.define('custom-button', CustomButton, { extends: 'button' });

View File

@ -0,0 +1,54 @@
import Store from '../modules/store.js';
class CustomItem extends HTMLOptionElement {
static observedAttributes = ['color'];
constructor() {
super();
this.publish = Store.publish;
this.checkedStyle = document.createElement("style");
this.appendChild(this.checkedStyle);
}
connectedCallback() {
this.updateStyle();
this.addEventListener('dblclick', this.onDoubleClick.bind(this));
}
disconnectedCallback() {
this.removeEventListener('dblclick', this.onDoubleClick);
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) return;
switch (name) {
case 'color':
this.checkedStyle.innerHTML = `
option:checked {
background: ${newValue} linear-gradient(0deg, ${newValue} 0%, ${newValue} 100%);
box-shadow: 0 0 10px 100px ${newValue} inset;
color: #fff;
outline: none;
}`;
break;
default:
console.error(`Unhandled attribute change: ${name}`);
}
}
updateStyle() {
this.style.height = '40px';
this.style.alignContent = 'center';
this.style.paddingLeft = '15px';
this.style.fontSize = '18px';
}
onDoubleClick() {
console.log('double click detected');
this.publish(Store.actions.REMOVE_LINE);
}
}
customElements.define('custom-item', CustomItem, { extends: 'option' });

View File

@ -0,0 +1,57 @@
class CustomModal extends HTMLElement {
static observedAttributes = ['active'];
#templateHtml = `
<div class="overlay">
<main-window width="700px" gap="25px">
<slot></slot>
</main-window>
</div>
`;
constructor() {
super();
this.attachShadow({ mode: 'open' });
const template = document.createElement('template')
template.innerHTML = this.#templateHtml;
this.shadowRoot.appendChild(template.content.cloneNode(true));
this.overlay = this.shadowRoot.querySelector('div.overlay');
this.mainWindow = this.shadowRoot.querySelector('main-window');
}
connectedCallback() {
this.updateStyle();
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) return;
switch (name) {
case 'active':
this.overlay.style.visibility = newValue === "true" ? 'visible' : 'hidden';
break;
default:
console.error(`Unhandled attribute change: ${name}`);
}
}
updateStyle() {
this.overlay.style.position = 'fixed';
this.overlay.style.margin = '0';
this.overlay.style.top = '0';
this.overlay.style.left = '0';
this.overlay.style.right = '0';
this.overlay.style.bottom = '0';
this.overlay.style.display = 'flex';
this.overlay.style.paddingTop = '50px';
this.overlay.style.justifyContent = 'center';
this.overlay.style.background = '#0000001A 0% 0% no-repeat padding-box';
this.overlay.style.opacity = '1';
this.overlay.style.transition = 'all 0.35s ease-in';
this.mainWindow.style.top = '25px';
}
}
customElements.define('custom-modal', CustomModal);

View File

@ -0,0 +1,31 @@
import Store from '../modules/store.js';
class CustomText extends HTMLInputElement {
constructor() {
super();
this.type = 'text';
}
connectedCallback() {
this.updateStyle();
this.addEventListener('input', this.onInput.bind(this));
}
disconnectedCallback() {
this.removeEventListener('input', this.onInput);
}
updateStyle() {
this.style.height = '60px';
this.style.fontSize = '18px';
this.style.fontFamily = 'inherit';
this.style.paddingLeft = '21px';
}
onInput() {
Store.publish(Store.actions.INPUT_TEXT, this.value);
}
}
customElements.define('custom-text', CustomText, { extends: 'input' });

View File

@ -1,35 +1,47 @@
import Store from '../modules/store.js';
class ItemList extends HTMLSelectElement {
static observedAttributes = ['height'];
constructor() {
super();
this.publish = Store.publish
this.publish = Store.publish;
}
connectedCallback() {
this.addEventListener('click', this.onChange.bind(this));
this.updateStyle();
}
onChange() {
const selectedIds = Array.from(this.selectedOptions).map((option) => +option.value);
this.publish(Store.actions.SELECT, selectedIds);
this.addEventListener('click', this.onChange.bind(this));
}
disconnectedCallback() {
this.removeEventListener('click', this.onChange.bind(this));
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) return;
switch (name) {
case 'height':
this.style.height = newValue;
break;
default:
console.error(`Unhandled attribute change: ${name}`);
}
}
updateStyle() {
this.style.width = '800px';
this.style.height = '227px';
this.style.background = '#F7F7F7 0% 0% no-repeat padding-box';
this.style.border = '1px solid #CCCCCC';
this.style.opacity = '1';
this.style.fontSize = '18px';
this.style.fontFamily = '"Montserrat" sans-serif';
this.style.fontWeight = 'normal';
this.style.letterSpacing = '0px';
this.style.width = '100%';
this.style.background = '#F7F7F7 0% 0% no-repeat padding-box';
this.style.border = '1px solid #CCCCCC';
this.style.opacity = '1';
this.style.fontSize = '18px';
this.style.padding = '13px';
this.style.boxSizing = 'border-box';
}
onChange() {
const selectedIds = Array.from(this.selectedOptions).map((option) => +option.value);
this.publish(this.getAttribute('action'), selectedIds);
}
}

View File

@ -1,73 +1,50 @@
class MainWindow extends HTMLElement {
static observedAttributes = ['title', 'description'];
static observedAttributes = ['width', 'gap'];
#templateHtml = `
<div class="window">
<slot></slot>
</div>
`;
constructor() {
super();
this.attachShadow({ mode: 'open' });
const template = document.createElement('template');
template.innerHTML = this.#templateHtml;
this.shadowRoot.appendChild(template.content.cloneNode(true));
this.window = this.shadowRoot.querySelector('div.window');
}
connectedCallback() {
this.render();
this.updateStyle();
}
attributeChangedCallback() {
this.render();
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) return;
switch (name) {
case 'width':
this.window.style.width = newValue;
break;
case 'gap':
this.window.style.rowGap = newValue;
break;
default:
console.error(`Unhandled attribute change: ${name}`);
}
}
render() {
const title = this.getAttribute('title') || "No title";
const description = this.getAttribute('description') || "No description";
this.shadowRoot.innerHTML = `
<style>
#window {
width: 900px;
height: 577px;
background: #FFFFFF 0% 0% no-repeat padding-box;
box-shadow: 0px 5px 12px #0000001F;
border-radius: 20px;
opacity: 1;
}
#content {
width: 800px;
margin: 0 50px 13px 50px;
}
h1 {
top: 221px;
margin: 50px 50px 13px 50px;
height: 49px;
text-align: center;
font-size: 40px;
font-family: "Montserrat", sans-serif;
font-weight: normal;
letter-spacing: 0px;
color: #333333;
opacity: 1;
}
p {
margin: 0 50px 0 50px;
text-align: center;
height: 74px;
/* UI Properties */
text-align: center;
font-size: 18px;
font-family: "Montserrat", sans-serif;
font-weight: normal;
letter-spacing: 0px;
color: #333333;
opacity: 1;
}
</style>
<div id="window">
<h1>${title}</h1>
<p>${description}</p>
<div id="content" >
<slot></slot>
<div/>
</div>
`;
updateStyle() {
this.window.style.boxSizing = 'border-box';
this.window.style.padding = '50px';
this.window.style.background = '#FFFFFF 0% 0% no-repeat padding-box';
this.window.style.boxShadow = '0px 5px 12px #0000001F';
this.window.style.borderRadius = '20px';
this.window.style.opacity = '1';
this.window.style.display = 'flex';
this.window.style.flexDirection = 'column';
}
}

View File

View File

@ -1,5 +1 @@
import './components/app-base.js';
import './components/main-window.js';
import './components/custom-button.js';
import './components/item-list.js';

View File

@ -4,16 +4,21 @@ export default class Store {
REMOVE_LINE: 'REMOVE_LINE',
UNDO: 'UNDO',
SELECT: 'SELECT',
OPEN_MODAL: 'OPEN_MODAL',
CLOSE_MODAL: 'CLOSE_MODAL',
INPUT_TEXT: 'INPUT_TEXT',
}
constructor() {
this.state = {
records: [],
selected: [],
input: "",
modalActive: false,
};
this.callbacks = [() => console.log(this.state)];
Object.values(this.constructor.actions).forEach(action => document.addEventListener(action, this.eventHandler.bind(this), false));
Object.values(this.constructor.actions).forEach(action => document.addEventListener(action, this.#eventHandler.bind(this), false));
}
subscribe(callback) {
@ -30,7 +35,7 @@ export default class Store {
document.dispatchEvent(event);
}
eventHandler(event) {
#eventHandler(event) {
if (!Object.values(this.constructor.actions).includes(event.type)) {
console.error(`Action "${event.type}" is not valid`);
return;
@ -38,24 +43,30 @@ export default class Store {
switch (event.type) {
case this.constructor.actions.ADD_LINE:
if (!this.state.input) break;
this.state = {
...this.state,
records: this.state.records.concat({
id: this.state.records.length,
text: 'asdf',
text: this.state.input,
action: 'add',
}),
input: "",
modalActive: false,
};
break;
case this.constructor.actions.REMOVE_LINE:
const linesToRemove = this.state.selected.map((id, index) => ({
id: this.state.records.length + index,
targetId: id,
action: 'remove',
}));
if (this.state.selected.length === 0) {
// Nothing to delete
break;
}
this.state = {
...this.state,
records: [ ...this.state.records, ...linesToRemove],
records: [ ...this.state.records, {
id: this.state.records.length,
targetIds: this.state.selected,
action: 'remove',
}],
selected: [],
};
break;
@ -71,6 +82,24 @@ export default class Store {
selected: event.detail,
}
break;
case this.constructor.actions.OPEN_MODAL:
this.state = {
...this.state,
modalActive: true,
}
break;
case this.constructor.actions.CLOSE_MODAL:
this.state = {
...this.state,
modalActive: false,
}
break;
case this.constructor.actions.INPUT_TEXT:
this.state = {
...this.state,
input: event.detail,
}
break;
default:
console.error(`Action "${event.type}" not implemented in eventHandler`);
return;
@ -85,7 +114,11 @@ export default class Store {
return acc.concat({id: current.id, text: current.text});
}
if (current.action === "remove") {
return acc.filter((item) => item.id !== current.targetId);
let temp = acc;
for (const id of current.targetIds) {
temp = temp.filter((item) => item.id !== id);
}
return temp;
}
console.error(`Action ${current.action} not valid`);
}, []);

View File

@ -1,4 +1,8 @@
export default {
title: 'This is a technical proof',
description: 'Lorem ipsum dolor sit amet consectetur adipiscing, elit mus primis nec inceptos. Lacinia habitasse arcu molestie maecenas cursus quam nunc, hendrerit posuere augue fames dictumst placerat porttitor, dis mi pharetra vestibulum venenatis phasellus.',
modal: {
description: 'Add item to list',
placeholder: 'Type the text here...',
},
}

View File

@ -8,87 +8,124 @@ export default class StoreTest {
test() {
(() => {
let called = false
const store = new Store(() => called = true);
const callback = () => called = true;
const store = new Store();
const event = new Event('addLine');
store.eventHandler(event);
store.subscribe(callback);
Store.publish(Store.actions.ADD_LINE);
this.assert("eventHandler calls the callback after successfull event", called, true);
this.assert("Subscribed callback is run after a successfull publish action", called, true);
})();
(() => {
const store = new Store(() => {});
store.records = [
const store = new Store();
store.state = {
records: [
{id: 0, text: "asdf", action: 'add'},
{id: 1, text: "qwer", action: 'add'},
],
selected: [0],
};
Store.publish('WRONG');
const expected = {
records: [
{id: 0, text: "asdf", action: 'add'},
{id: 1, text: "qwer", action: 'add'},
],
selected: [0],
};
this.assert("Publish with unknown action does nothing", store.state, expected);
})();
(() => {
const store = new Store();
Store.publish(Store.actions.ADD_LINE);
const expected = [
{id: 0, text: "asdf", action: 'add'},
];
this.assert("Publishing action ADD_LINE adds a line to the records", store.state.records, expected);
})();
(() => {
const store = new Store();
store.state = {
records: [
{id: 0, text: "asdf", action: 'add'},
],
selected: [],
};
Store.publish(Store.actions.REMOVE_LINE);
const expected = [
{id: 0, text: "asdf", action: 'add'},
];
this.assert("Publish action REMOVE_LINE without selected in the state does nothing", store.state.records, expected);
})();
(() => {
const store = new Store();
store.state = {
records: [
{id: 0, text: "asdf", action: 'add'},
],
selected: [0],
};
Store.publish(Store.actions.REMOVE_LINE);
const expected = [
{id: 0, text: "asdf", action: 'add'},
{id: 1, targetIds: [0], action: 'remove'},
];
this.assert("Publish action REMOVE_LINE with one selected id in the state creates a remove record", store.state.records, expected);
this.assert("Selected status is reset after REMOVE_LINE action", store.state.selected, []);
})();
(() => {
const store = new Store();
store.state.records = [
{id: 0, text: "asdf", action: 'add'},
{id: 1, text: "qwer", action: 'add'},
{id: 2, action: 'remove'},
]
const event = new Event('unknown');
store.eventHandler(event);
const expected = [
{id: 0, text: "asdf", action: 'add'},
{id: 1, text: "qwer", action: 'add'},
{id: 2, action: 'remove'},
];
this.assert("eventHandler does nothing on an unknown event type", store.records, expected);
})();
(() => {
const store = new Store(() => {});
const event = new Event('addLine');
store.eventHandler(event);
const expected = [
{id: 0, text: "asdf", action: 'add'},
];
this.assert("eventHandler adds the appropiate record when Event 'addLine' is passed", store.records, expected);
})();
(() => {
const store = new Store(() => {});
store.records = [
{id: 0, text: "asdf", action: 'add'},
]
const event = new Event('removeLine');
store.eventHandler(event);
const expected = [
{id: 0, text: "asdf", action: 'add'},
{id: 1, action: 'remove'},
];
this.assert("eventHandler adds the appropiate record when Event 'removeLine' is passed", store.records, expected);
})();
(() => {
const store = new Store(() => {});
store.records = [
{id: 0, text: "asdf", action: 'add'},
{id: 1, text: "qwer", action: 'add'},
{id: 2, action: 'remove'},
]
const event = new Event('undo');
store.eventHandler(event);
Store.publish(Store.actions.UNDO);
const expected = [
{id: 0, text: "asdf", action: 'add'},
{id: 1, text: "qwer", action: 'add'},
];
this.assert("eventHandler deletes the appropiate record when Event 'undo' is passed", store.records, expected);
this.assert("Publish action UNDO removes last record", store.state.records, expected);
})();
(() => {
const store = new Store(() => {});
store.records = [
(() => {
const store = new Store();
store.state.selected = [0]
Store.publish(Store.actions.SELECTED, [1, 3]);
const expected = [1, 3];
this.assert("Publish action SELECTED updates the array of selected ids", store.state.records, expected);
})();
(() => {
const store = new Store();
store.state.records = [
{id: 0, text: "Line 1", action: 'add'},
{id: 1, text: "Line 2", action: 'add'},
{id: 2, text: "Line 3", action: 'add'},
@ -100,17 +137,17 @@ export default class StoreTest {
{id: 2, text: "Line 3"},
];
this.assert("getState returns an array of strings with 3 add records", store.getState(), expected);
this.assert("getRecords() returns an array of strings with 3 add records", store.getRecords(), expected);
})();
(() => {
const store = new Store(() => {});
const store = new Store();
store.records = [
store.state.records = [
{id: 0, text: "Line 1", action: 'add'},
{id: 1, text: "Line 2", action: 'add'},
{id: 2, text: "Line 3", action: 'add'},
{id: 3, targetId: 1, action: 'remove'},
{id: 3, targetIds: [1], action: 'remove'},
];
const expected = [
@ -118,7 +155,34 @@ export default class StoreTest {
{id: 2, text: "Line 3"},
];
this.assert("getState returns an array of strings with 3 add records and a delete record", store.getState(), expected);
this.assert("getRecords() returns an array of 2 strings with 3 add records and a delete record with 1 id", store.getRecords(), expected);
})();
(() => {
const store = new Store();
store.state.records = [
{id: 0, text: "Line 1", action: 'add'},
{id: 1, text: "Line 2", action: 'add'},
{id: 2, text: "Line 3", action: 'add'},
{id: 3, targetIds: [0, 1], action: 'remove'},
];
const expected = [
{id: 2, text: "Line 3"},
];
this.assert("getRecords() returns an array of 1 string with 3 add records and a delete record with 2 ids", store.getRecords(), expected);
})();
(() => {
const store = new Store();
store.state.selected = [0, 1];
const expected = [0, 1];
this.assert("getSelected() returns an array of ids representing the selected items", store.getSelected(), expected);
})();
}
}