Version remaniée de la classe principale + réécriture de ses tests.

This commit is contained in:
Fabrice PENHOËT 2021-10-26 18:01:23 +02:00
parent 8c0b9b2aa2
commit 739b83f365
6 changed files with 393 additions and 340 deletions

View File

@ -1,6 +1,6 @@
{
"name": "freedatas2html",
"version": "0.9.0",
"version": "0.9.5",
"description": "Conversion and display of data in different formats (CSV, JSON, HTML) with the possibility of filtering, classifying and paginating the results.",
"main": "index.js",
"scripts": {

View File

@ -1,7 +1,7 @@
const { compare }=require('natural-orderby');
const { compare }=require("natural-orderby");
const errors=require("./errors.js");
import { Counter, DatasRenders, DOMElement, Filters, Paginations, Parsers, ParseErrors, RemoteSources, SortingFields, SortingFunctions } from "./interfaces";
import { DatasRenders, DOMElement, Filters, Paginations, Parsers, ParseResults, RemoteSources, SortingFields, SortingFunctions } from "./interfaces";
import { Pagination} from "./Pagination";
import { ParserForCSV} from "./ParserForCSV";
import { ParserForHTML} from "./ParserForHTML";
@ -12,62 +12,50 @@ import { SortingField } from "./SortingField";
export class FreeDatas2HTML
{
// L'élément HTML où afficher les données. Laisser à undefined si non affichées :
// Les paramètres de base :
private _datasViewElt: DOMElement|undefined=undefined;
// Le moteur de rendu pour préparer l'affichage des données
public datasRender: DatasRenders;
// Le code HTML résultant :
public datasHTML: string = "";
// Le parseur :
public parser: Parsers; // public pour permettre de charger un parseur tiers après instanciation
// Type de données à traiter
public datasType: "CSV"|"HTML"|"JSON"|undefined;
public parser: Parsers;
public stopIfParseErrors: boolean=false;
// Le nom des champs trouvés dans les données :
public fields: string[]|undefined=undefined;
// Les données à proprement parler :
public datas: {[index: string]:string}[]=[];
// Les erreurs rencontrées durant le traitement des données reçues :
public parseErrors: ParseErrors[]|undefined;
// Doit-on tout arrêter si une erreur est rencontrée durant le traitement ?
public stopIfParseErrors: boolean = false;
// Les options (classement, pagination, filtres...) :
private _datasCounterElt: DOMElement|undefined=undefined;
private _datasSortingFunctions: SortingFunctions[]=[];
public datasFilters: Filters[]=[];
public datasSortingFields: SortingFields[]=[];
public datasSortedField: SortingFields|undefined;
public pagination: Paginations|undefined;
// Les fonctions spécifiques de classement pour certains champs :
private _datasSortingFunctions: SortingFunctions[] = [];
// Les filtres possible sur certains champs :
datasFilters: Filters[] = [];
// Les champs pouvant être classés :
datasSortingFields: SortingFields[] = [];
// La dernier champ pour lequel le classement a été demandé :
datasSortedField: SortingFields|undefined;
// Éventuelle pagination :
pagination: Paginations|undefined;
// Affichage du nombre total de lignes de données (optionnel) :
private _datasCounter: Counter = {};
// Les résultats :
private _fields: ParseResults["fields"]=[];
private _datas: ParseResults["datas"]=[];
private _datas2Rend: {[index: string]:string}[]=[];
private _nbDatasValid: number=0;
// J'initialiser avec des valeurs par défaut pouvant être surchargées par les setters
// Attention, si je transmets datasRemoteSource ici, il ne passera pas par un new RemoteSources()
// Il doit donc déjà avoir été testé
constructor(datasType:"CSV"|"HTML"|"JSON", datas2Parse="", datasRemoteSource?:RemoteSources)
// Le parseur, comme le render sont initialisés, mais peuvent être modifiés par des instances d'autres classes respectant leur interface.
constructor(datasFormat:"CSV"|"HTML"|"JSON", datas2Parse="", datasRemoteSource?:RemoteSources)
{
this.datasRender=new Render();
switch (datasType)
switch (datasFormat)
{
case "CSV":
this.parser=new ParserForCSV(datasRemoteSource);
this.parser=new ParserForCSV();
break;
case "HTML":
this.parser=new ParserForHTML(datasRemoteSource);
this.parser=new ParserForHTML();
break;
case "JSON":
this.parser=new ParserForJSON(datasRemoteSource);
this.parser=new ParserForJSON();
break;
}
if(datas2Parse.trim() !== "")
this.parser.datas2Parse=datas2Parse.trim();
else if(datasRemoteSource !== undefined)
this.parser.setRemoteSource(datasRemoteSource);
}
// Vérifie s'il y a bien un élément dans le DOM pour l'id fourni
// Vérifie s'il y a bien un élément dans le DOM pour l'id fourni.
// Fonction statique également utilisée par les autres classes.
public static checkInDOMById(checkedElt: DOMElement) : DOMElement
{
let searchEltInDOM=document.getElementById(checkedElt.id);
@ -80,23 +68,48 @@ export class FreeDatas2HTML
}
}
// Vérifie qu'un champ existe bien dans les données
public checkFieldExist(nb: number) : boolean
{
if(this.fields === undefined || this.fields[nb] === undefined)
return false;
else
return true;
}
// Vérifie que l'élément devant afficher les données existe dans le DOM :
set datasViewElt(elt: DOMElement)
{
this._datasViewElt=FreeDatas2HTML.checkInDOMById(elt);
}
// Vérifie que les numéros de champs pour lesquels il y a des fonctions de classement spécifiques sont cohérents
// ! Ne peut être testé qu'après avoir reçu les données
set datasCounterElt(counterDisplayElement: DOMElement)
{
this._datasCounterElt=FreeDatas2HTML.checkInDOMById(counterDisplayElement);
}
get datas(): ParseResults["datas"]
{
return this._datas;
}
get fields(): ParseResults["fields"]
{
return this._fields;
}
get datas2Rend(): {[index: string]:string}[]
{
return this._datas2Rend;
}
get nbDatasValid(): number
{
return this._nbDatasValid;
}
// Vérifie qu'un champ existe bien dans les données parsées.
// Utilisée par les autres classes.
public checkFieldExist(nb: number) : boolean
{
if(this.parser.parseResults === undefined || this.parser.parseResults.fields[nb] === undefined)
return false;
else
return true;
}
// Vérifie que les numéros de champs pour lesquels il y a des fonctions de classement spécifiques sont cohérents.
// ! Ne peut donc être utilisé qu'après avoir parsé les données.
set datasSortingFunctions(SortingFunctions: SortingFunctions[])
{
this._datasSortingFunctions=[];
@ -109,21 +122,6 @@ export class FreeDatas2HTML
}
}
// On teste l'id de l'élément du DOM où afficher le compteur s'il est fourni
set datasCounter(counterDisplayElement: DOMElement)
{
this._datasCounter={ displayElement: FreeDatas2HTML.checkInDOMById(counterDisplayElement), value: undefined };
}
// Retourne la valeur du compteur de lignes (sans l'élément DOM)
public getDatasCounterValue(): number|undefined
{
if(this._datasCounter !== undefined && this._datasCounter.value != undefined)
return this._datasCounter.value;
else
return undefined;
}
// Retourne l'éventuelle fonction spécifique de classement associée à un champ
public getSortingFunctionForField(datasFieldNb: number): SortingFunctions|undefined
{
@ -135,27 +133,22 @@ export class FreeDatas2HTML
return undefined;
}
// Traite les données fournies via le parseur adhoc
// Si un élément du DOM est fourni, appelle la fonction affichant les données
// Lancer le parsage des données et lance éventuellement un 1er affichage.
public async run(): Promise<any>
{
await this.parser.parse();
if(this.parser.parseResults === undefined)
if(this.parser.parseResults === undefined) // mais le parseur devrait lui-même générer une erreur avant
throw new Error(errors.parserFail);
else
{
if(this.parser.parseResults.fields === undefined)
throw new Error(errors.parserDatasNotFound);
else if(this.stopIfParseErrors && this.parser.parseResults.errors !== undefined)
if(this.stopIfParseErrors && this.parser.parseResults.errors !== undefined)
throw new Error(errors.parserMeetErrors);
else
{
// revoir l'intérêt de copier ces 3 attributs ?
this.fields=this.parser.parseResults.fields;
this.datas=this.parser.parseResults.datas;
this.parseErrors=this.parser.parseResults.errors;
// Les champs ne bougeront plus donc on peut aussi les passer au moteur de rendu
this.datasRender.fields=this.fields;
this._fields=this.parser.parseResults.fields;
this._datas=this.parser.parseResults.datas;
// Les champs ne bougeront plus, donc on peut déjà les passer au moteur de rendu :
this.datasRender.fields=this._fields;
if(this._datasViewElt !== undefined)
this.refreshView();
return true;
@ -163,81 +156,83 @@ export class FreeDatas2HTML
}
}
// Actualise l'affichage des données.
// Méthode également appelée par les autres classes.
public refreshView() : void
{
if(this.fields === undefined || this._datasViewElt === undefined || this._datasViewElt.eltDOM === undefined)
if(this._fields.length === 0 || this._datasViewElt === undefined)
throw new Error(errors.converterRefreshFail);
else
{
this.datasHTML=this.createDatas2Display(this.fields, this.datas);
this._datasViewElt.eltDOM.innerHTML=this.datasHTML;
// On réactive les éventuels champs de classement qui ont été écrasés
this._datas2Rend=this.datas2HTML();
this.datasRender.datas= this._datas2Rend;
this._datasViewElt.eltDOM!.innerHTML=this.datasRender.rend2HTML(); // "!", car l'existence de "eltDOM" est testée par le setter.
// Actualisation de l'éventuel compteur :
if(this._datasCounterElt !== undefined)
this._datasCounterElt.eltDOM!.innerHTML=""+this._nbDatasValid; // même remarque pour le "!".
// Réactivation des éventuels champs de classement qui ont pu être écrasés :
for(let field of this.datasSortingFields)
field.field2HTML();
// Tout réaffichage peut entraîner une modification du nombre de pages (évolution filtres, etc.)
if(this.pagination !== undefined)
this.pagination.pages2HTML();
}
}
private createDatas2Display(fields: string[], datas: any[]) : string
// Fonction sélectionnant les données à afficher en prenant en compte les éventuels filtres, la pagination, etc.
public datas2HTML() : {[index: string]:string}[]
{
// Dois-je classer les données par rapport à un champ ?
if(this.datasSortedField !== undefined && this.datasSortedField.datasFieldNb !==undefined)
if(this.datasSortedField !== undefined)
{
const field=fields[this.datasSortedField.datasFieldNb];
const field=this._fields[this.datasSortedField.datasFieldNb];
const fieldOrder=this.datasSortedField.order;
// Une fonction spécifique de classement a-t-elle été définie ?
// Une fonction spécifique de classement a-t-elle été définie pour ce champ ?
if(this.getSortingFunctionForField(this.datasSortedField.datasFieldNb) !== undefined)
{
let myFunction=this.getSortingFunctionForField(this.datasSortedField.datasFieldNb);
datas.sort( (a, b) => { return myFunction!.sort(a[field], b[field], fieldOrder); });
const myFunction=this.getSortingFunctionForField(this.datasSortedField.datasFieldNb);
this._datas.sort( (a, b) => { return myFunction!.sort(a[field], b[field], fieldOrder); });
}
else
datas.sort( (a, b) => compare( {order: fieldOrder} )(a[field], b[field]));
this._datas.sort( (a, b) => compare( {order: fieldOrder} )(a[field], b[field]));
}
// Dois-je prendre en compte une pagination ?
let firstData=0;
if (this.pagination !== undefined && this.pagination.selectedValue !== undefined && this.pagination.pages !== undefined && this.pagination.pages.selectedValue !== undefined)
firstData=this.pagination.selectedValue*(this.pagination.pages.selectedValue-1);
let maxData = (this.pagination !== undefined && this.pagination.selectedValue !== undefined) ? this.pagination.selectedValue : datas.length+1;
let maxData=(this.pagination !== undefined && this.pagination.selectedValue !== undefined) ? this.pagination.selectedValue : this._datas.length;
// Création du tableau des données à afficher :
const datas2Display=[];
let nbVisible=0, nbTotal=0;
for (let row in datas)
for (let row in this._datas)
{
let visible=true;
if(this.datasFilters.length !== 0)
// Pour être affichée une ligne doit valider tous les filtres connus
let valid=true, i=0;
while(this.datasFilters[i] !== undefined && valid === true)
{
let i=0;
while(this.datasFilters[i] !== undefined && visible===true)
{
visible=this.datasFilters[i].dataIsOk(datas[row]);
valid=this.datasFilters[i].dataIsOk(this._datas[row]);
i++;
}
}
if(visible && nbTotal >= firstData && nbVisible < maxData)
if(valid && nbTotal >= firstData && nbVisible < maxData)
{
datas2Display.push(datas[row]);
datas2Display.push(this._datas[row]);
nbVisible++;
nbTotal++;
}
else if(visible)
else if(valid)
nbTotal++;
}
if(this._datasCounter !== undefined && this._datasCounter.displayElement !== undefined)
{
this._datasCounter.value=nbTotal;
this._datasCounter.displayElement.eltDOM!.innerHTML=""+nbTotal; // eltDOM ne peut être undefined (cf setter)
}
// Tout réaffichage peut entraîner une modification du nombre de pages (évolution filtres, etc.)
if(this.pagination !== undefined)
this.pagination.pages2HTML(nbTotal);
this.datasRender.datas=datas2Display;
return this.datasRender.rend2HTML();
this._nbDatasValid=nbTotal;
return datas2Display;
}
}
// Permet l'appel des dépendances via un seul script
// Permet l'appel des principales classes du module via un seul script :
export { Pagination } from "./Pagination";
export { Render} from "./Render";
export { Selector } from "./Selector";

View File

@ -33,7 +33,7 @@ module.exports =
selectorFieldIsEmpty: "Aucune donnée trouvée pour le champ du filtre",
selectorFieldNotFound: "Au moins un des champs devant servir à filtrer les données n'existe pas dans le fichier.",
selectorSelectedIndexNotFound: "La valeur sélectionnée n'a pas été trouvée dans la liste des champs.",
sortingFieldNeedDatas: "Le création d'un champ de classement nécessite la transmission de la liste des champs.",
sortingFieldNeedDatas: "La création d'un champ de classement nécessite la transmission de la liste des champs.",
sortingFieldNotFound: "Au moins un des champs devant permettre de classer les données n'existe pas dans le fichier.",
sortingFieldsNbFail: "Le nombre de champs trouvés dans le DOM ne correspond pas à celui des données à classer.",
sortingFieldsNotInHTML: "Les champs pouvant servir à classer les données n'ont pas été trouvés dans le DOM.",

View File

@ -1,8 +1,3 @@
export interface Counter
{
displayElement?: DOMElement; // peut être undefined si on ne souhaite pas d'affichage automatique dans la page
value?: number; // undefined jusqu'à recevoir sa première valeur
}
export interface DatasRenders
{
fields: string[];
@ -39,7 +34,7 @@ export interface Paginations
selectedValue: number|undefined;
pages: PaginationsPages;
options2HTML(): void;
pages2HTML(nbTotal:number) : void;
pages2HTML() : void;
}
export interface PaginationsOptions
{

View File

@ -11,7 +11,6 @@ module.exports =
sortingColumn1HTML: '<a href="#freeDatas2HTMLSorting0" id="freeDatas2HTMLSorting0">Z (numéro atomique)</a>',
sortingColumn2HTML: '<a href="#freeDatas2HTMLSorting2" id="freeDatas2HTMLSorting2">Symbole</a>',
selectorForPagination: '<label for="freeDatas2HTMLPaginationSelector">Choix de pagination : </label><select name="freeDatas2HTMLPaginationSelector" id="freeDatas2HTMLPaginationSelector"><option value="0">----</option><option value="1">10</option><option value="2">20</option><option value="3">50</option><option value="4">500</option></select>',
selectorFor2Pages: '<label for="freeDatas2HTMLPagesSelector">Page à afficher :</label><select name="freeDatas2HTMLPagesSelector" id="freeDatas2HTMLPagesSelector"><option value="1">1</option><option value="2">2</option></select>',
selectorForManyPages: '<label for="freeDatas2HTMLPagesSelector">Page à afficher :</label><select name="freeDatas2HTMLPagesSelector" id="freeDatas2HTMLPagesSelector"><option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option><option value="6">6</option><option value="7">7</option><option value="8">8</option><option value="9">9</option><option value="10">10</option><option value="11">11</option><option value="12">12</option></select>',
firstLineForPageSelection1:"<td>51</td><td>Antimoine</td><td>Sb</td><td>Métalloïde</td><td>&gt; 1 et &lt; 100 000</td>",
lastLineForPageSelection1:"<td>100</td><td>Fermium</td><td>Fm</td><td>Actinide</td><td>Inexistant</td>",

View File

@ -1,18 +1,20 @@
import { FreeDatas2HTML, Render} from "../src/FreeDatas2HTML";
import { FreeDatas2HTML, Pagination, Render, Selector, SortingField } from "../src/FreeDatas2HTML";
import { ParserForCSV} from "../src/ParserForCSV";
import { ParserForHTML} from "../src/ParserForHTML";
import { ParserForJSON} from "../src/ParserForJSON";
import { RemoteSource} from "../src/RemoteSource";
const { compare }=require("natural-orderby");
const errors=require("../src/errors.js");
const fixtures=require("./fixtures.js");
/// EN CHANTIER !!!
/// Tests à revoir après avoir fait le tour des autres classes
/*
describe("Test du script central de FreeDatas2HTML", () =>
describe("Tests du script central de FreeDatas2HTML", () =>
{
let converter: FreeDatas2HTML;
beforeEach( () =>
{
converter=new FreeDatas2HTML("CSV");
converter.parser.setRemoteSource({ url:"http://localhost:9876/datas/datas1.csv" });
document.body.insertAdjacentHTML('afterbegin', fixtures.datasViewEltHTML);
});
@ -26,26 +28,139 @@ describe("Test du script central de FreeDatas2HTML", () =>
expect(converter).toBeInstanceOf(FreeDatas2HTML);
});
describe("Test des données de configuration reçues.", () =>
describe("Test des paramètres de configuration reçus.", () =>
{
it("Doit générer une erreur s'il n'y a pas d'élément dans la page pour l'id fourni.", () =>
it("Doit instancier le bon parseur.", () =>
{
converter=new FreeDatas2HTML("CSV");
expect(converter.parser).toBeInstanceOf(ParserForCSV);
converter=new FreeDatas2HTML("HTML");
expect(converter.parser).toBeInstanceOf(ParserForHTML);
converter=new FreeDatas2HTML("JSON");
expect(converter.parser).toBeInstanceOf(ParserForJSON);
});
it("S'il est fourni une chaîne vide comme données à parser, elle ne doit pas être passée au parseur.", () =>
{
converter=new FreeDatas2HTML("CSV", "");
expect(converter.parser.datas2Parse).toEqual("");
// Idem avec espaces bidons :
converter=new FreeDatas2HTML("CSV", " ");
expect(converter.parser.datas2Parse).toEqual("");
});
it("S'il est fourni une chaîne de caractères valide, elle doit être passée au parseur.", () =>
{
converter=new FreeDatas2HTML("CSV", "datas");
expect(converter.parser.datas2Parse).toEqual("datas");
});
it("Si une source de données distante est fournie en paramètre, elle doit être passée en parseur.", () =>
{
const remoteSource=new RemoteSource({ url:"http://localhost:9876/datas/datas1.csv" });
converter=new FreeDatas2HTML("CSV", "", remoteSource);
expect(converter.parser.datasRemoteSource).toEqual(remoteSource);
});
it("Doit générer une erreur s'il n'est pas trouvé d'élément dans la page pour l'id fourni.", () =>
{
expect(() => { return FreeDatas2HTML.checkInDOMById({ id:"dontExist" }); }).toThrowError(errors.converterElementNotFound+"dontExist");
});
it("S'il y a bien un élément dans la page pour l'id fourni, doit retourner l'élement DOM complété.", () =>
it("S'il y a bien un élément trouvé dans la page pour l'id fourni, doit retourner l'élement DOM complété.", () =>
{
const eltInDOM=document.getElementById("datas");
const checkElt=FreeDatas2HTML.checkInDOMById({ id:"datas" });
expect(checkElt).toEqual({ id:"datas", eltDOM: eltInDOM });
});
});
it("Doit retourner false si un numéro de champ n'est pas trouvé dans les données.", async () =>
describe("Parsage et récupération des données.", () =>
{
let check=converter.checkFieldExist(2); // aucune donnée chargée, donc le champ ne peut être trouvé
expect(check).toBeFalse();
beforeEach( async () =>
{
converter.parser.setRemoteSource({ url:"http://localhost:9876/datas/datas1.csv" });
await converter.run();
check=converter.checkFieldExist(-2);
});
it("Doit générer une erreur si le parseur ne retourne aucun résultat.", async () =>
{
converter=new FreeDatas2HTML("CSV");
spyOn(converter.parser, "parse"); // bloque le fonctionnement de parse()
await expectAsync(converter.run()).toBeRejectedWith(new Error(errors.parserFail));
});
it("Doit générer une erreur si des anomalies sont rencontrées durant le parsage et que cela n'est pas toléré.", async () =>
{
const remoteSource=new RemoteSource({ url:"http://localhost:9876/datas/datas-errors1.csv" });
converter=new FreeDatas2HTML("CSV", "", remoteSource);
converter.stopIfParseErrors=true;
await expectAsync(converter.run()).toBeRejectedWith(new Error(errors.parserMeetErrors));
});
it("Ne doit pas générer une erreur si des anomalies sont rencontrées durant le parsage, mais que cela est toléré.", async () =>
{
const remoteSource=new RemoteSource({ url:"http://localhost:9876/datas/datas-errors1.csv" });
converter=new FreeDatas2HTML("CSV", "", remoteSource);
await expectAsync(converter.run()).toBeResolved();
});
it("Si le parsage s'est bien déroulé, le résultat doit être récupéré.", () =>
{
expect(converter.datas).toEqual(converter.parser.parseResults.datas);
expect(converter.fields).toEqual(converter.parser.parseResults.fields);
});
it("Si le parsage s'est bien déroulé, la liste des champs trouvés doit être transmise au moteur de rendu sans altération.", () =>
{
expect(converter.datasRender.fields).toEqual(converter.parser.parseResults.fields);
});
it("Si le parsage s'est bien déroulé et qu'un élément HTML est renseigné pour recevoir les données, un premier affichage doit être demandé.", async () =>
{
spyOn(converter, "refreshView");
converter.datasViewElt={ id:"datas" };
await converter.run();
expect(converter.refreshView).toHaveBeenCalled();
});
it("Si le parsage s'est bien déroulé, mais qu'aucun élément HTML n'est renseigné pour recevoir les données, l'affichage ne doit pas être demandé.", async () =>
{
spyOn(converter, "refreshView");
await converter.run();
expect(converter.refreshView).not.toHaveBeenCalled();
});
});
describe("Tests et configurations après parsage.", () =>
{
let simpleSort: (a: number, b: number) => number;
beforeEach( async () =>
{
converter.parser.setRemoteSource({ url:"http://localhost:9876/datas/datas1.csv" });
await converter.run();
simpleSort = (a: number, b: number) =>
{
if(a < b)
return 1;
else if(a > b)
return -1;
else
return 0;
};
});
it("Le test d'existence d'un champ doit retourner false s'il est lancé avant que les données n'aient été parsées.", () =>
{
converter=new FreeDatas2HTML("CSV");
const check=converter.checkFieldExist(0);
expect(converter.checkFieldExist(0)).toBeFalse();
// Dans le cas d'un parsage ne retournant rien, c'est le parseur qui va générer une erreur.
});
it("Doit retourner false si le numéro de champ n'est pas trouvé dans les données.", () =>
{
let check=converter.checkFieldExist(-2);
expect(check).toBeFalse();
check=converter.checkFieldExist(1.1);
expect(check).toBeFalse();
@ -53,149 +168,135 @@ describe("Test du script central de FreeDatas2HTML", () =>
expect(check).toBeFalse();
});
it("Doit retourner true si un numéro de champ est bien trouvé dans les données.", async () =>
it("Doit retourner true si le numéro de champ est bien trouvé dans les données.", () =>
{
await converter.run();
let check=converter.checkFieldExist(0);
expect(check).toBeTrue();
check=converter.checkFieldExist(2);
expect(check).toBeTrue();
});
it("Doit générer une erreur si une fonction est associée à un champ n'existant pas dans les données.", async () =>
it("Doit générer une erreur si une fonction est associée à un champ n'existant pas dans les données.", () =>
{
const simpleSort = (a: any, b: any) =>
{
if(a < b)
return 1;
else if(a > b)
return -1;
else
return 0;
};
expect(() => { return converter.datasSortingFunctions=[{ datasFieldNb:0, sort:simpleSort }]; }).toThrowError(errors.converterFieldNotFound); // données non chargées
converter=new FreeDatas2HTML("CSV");
converter.parser.setRemoteSource({ url:"http://localhost:9876/datas/datas1.csv" });
await converter.run();
expect(() => { return converter.datasSortingFunctions=[{ datasFieldNb:10, sort:simpleSort }]; }).toThrowError(errors.converterFieldNotFound);
});
it("Doit accepter la fonction associée à un champ, de manière à ce qu'elle soit utilisable pour comparer deux valeurs.", async () =>
it("Doit accepter la fonction associée à un champ, de manière à ce qu'elle soit utilisable pour comparer deux valeurs.", () =>
{
const simpleSort = (a: any, b: any) =>
{
if(a < b)
return 1;
else if(a > b)
return -1;
else
return 0;
};
await converter.run();
converter.datasSortingFunctions=[{ datasFieldNb:0, sort:simpleSort }];
expect(() => { return converter.datasSortingFunctions=[{ datasFieldNb:0, sort:simpleSort }]; }).not.toThrowError();
expect(converter.getSortingFunctionForField(0)).toBeDefined();
expect([7,9,3,5].sort(converter.getSortingFunctionForField(0).sort)).toEqual([9,7,5,3]);
});
});
describe("Affichage des données reçues.", () =>
describe("Fonction actualisant l'affichage.", () =>
{
it("Doit générer une erreur si des données n'ont pas été importées.", async () =>
beforeEach( async () =>
{
// Parseur non lancé :
expect(() => { return converter.refreshView(); }).toThrowError(errors.converterRefreshFail);
// Lancé, mais sur un fichier vide : à revoir, car c'est le parseur qui génère d'abord une erreur
//converter=new FreeDatas2HTML("CSV","", { url:"http://localhost:9876/datas/nodatas.csv"});
//await converter.run();
//expect(() => { return converter.refreshView(); }).toThrowError(errors.converterRefreshFail);
converter.parser.setRemoteSource({ url:"http://localhost:9876/datas/datas1.csv" });
await converter.run();// récupére les données sans actualiser affichage car élement HTML non connu
converter.datasViewElt={ id:"datas" }; // pour la suite, si ! :)
});
it("Doit générer une erreur si l'élément du DOM où afficher les données est inconnu.", async () =>
it("Doit générer une erreur si appelée avant d'avoir récupérer des données à afficher.", () =>
{
converter=new FreeDatas2HTML("CSV");
converter.datasViewElt={ id:"datas" };
expect(() => { return converter.refreshView(); }).toThrowError(errors.converterRefreshFail);
});
it("Doit générer une erreur si appelée sans avoir fourni d'élément HTML où afficher les données.", async () =>
{
converter=new FreeDatas2HTML("CSV");
converter.parser.setRemoteSource({ url:"http://localhost:9876/datas/datas1.csv" });
await converter.run();
expect(() => { return converter.refreshView(); }).toThrowError(errors.converterRefreshFail);
});
it("Ne doit pas générer d'erreur si les informations nécessaires sont ok.", async () =>
it("Doit appelé la fonction préparant les données à afficher et transmettre le résultat au moteur de rendu.", () =>
{
spyOn(converter, "datas2HTML").and.callThrough();
converter.refreshView();
expect(converter.datas2HTML).toHaveBeenCalled();
expect(converter.datasRender.datas).toEqual(converter.datas2Rend);
});
it("Doit appelé le moteur de rendu et afficher le résultat dans la page.", async () =>
{
converter=new FreeDatas2HTML("CSV", "name,firstname,birthday\ndoe,john,2000/12/25");
await converter.run();// parse sans rien afficher
converter.datasViewElt={ id:"datas" };
spyOn(converter.datasRender, "rend2HTML").and.callThrough();
converter.refreshView();
expect(converter.datasRender.rend2HTML).toHaveBeenCalled();
// Les données à afficher doivent être assez simples, car certains caractères peuvent être remplacés par innerHTML (exemples :"<" ou ">")
expect(document.getElementById("datas").innerHTML).toEqual(converter.datasRender.rend2HTML());
});
it("Si un élément HTML devant affiché le nombre de résultats est connu, il doit être actualisé.", () =>
{
converter.datasCounterElt={ id: "counter" };
converter.refreshView();
expect(document.getElementById("counter").innerHTML).toEqual(""+converter.nbDatasValid);
});
it("Si des champs de classement existent, leur code HTML doit être actualisé.", () =>
{
converter.refreshView(); // nécessaire pour que les champs soit trouvés dans le HTML
const sortingField1=new SortingField(converter, 0);
const sortingField2=new SortingField(converter, 1);
converter.datasSortingFields=[sortingField1,sortingField2];
spyOn(sortingField1, "field2HTML");
spyOn(sortingField2, "field2HTML");
converter.refreshView();
expect(sortingField1.field2HTML).toHaveBeenCalled();
expect(sortingField2.field2HTML).toHaveBeenCalled();
});
it("Si une pagination est configurée, le code HTML listant les pages doit être actualisé.", () =>
{
const pagination=new Pagination(converter, { id:"pages" }, "Page à afficher :");
converter.pagination=pagination;
spyOn(pagination, "pages2HTML");
converter.refreshView();
expect(pagination.pages2HTML).toHaveBeenCalled();
});
});
describe("Fonction filtrant les données à afficher.", () =>
{
beforeEach( async () =>
{
converter.parser.setRemoteSource({ url:"http://localhost:9876/datas/datas1.csv" });
converter.datasViewElt={ id:"datas" };
await converter.run();
expect(() => { return converter.refreshView(); }).not.toThrowError();
});
it("Doit générer une erreur, si la moindre erreur est rencontrée durant la parsage et que cela n'est pas accepté.", async () =>
it("Si un champ de classement est activé par l'utilisateur, les données doivent être classées via ce champ.", () =>
{
converter=new FreeDatas2HTML("CSV");
converter.parser.setRemoteSource({ url:"http://localhost:9876/datas/datas-errors1.csv" });
converter.stopIfParseErrors=true;
await expectAsync(converter.run()).toBeRejectedWith(new Error(errors.parserMeetErrors));
});
it("Si cela n'est pas demandé, le script ne sera pas bloqué, même si des erreurs sont rencontrées durant le parsage.", async () =>
{
converter.datasViewElt={ id:"datas" };
converter=new FreeDatas2HTML("CSV");
converter.parser.setRemoteSource({ url:"http://localhost:9876/datas/datas-errors1.csv" });
await expectAsync(converter.run()).toBeResolved();
});
/// Plutôt pour tester Render
it("Doit afficher un tableau correspondant aux données du fichier csv", async () =>
{
converter.datasViewElt={ id:"datas" };
await converter.run();
const render=new Render();
render.datas=converter.datas;
const htmlForDatas=render.rend2HTML();
// On ne peut comparer directement au contenu du DOM,
// car le navigateur change certains caractères (exemple : ">" devient "&gt;")
expect(converter.datasHTML).toEqual(htmlForDatas);
// Mais le code commence tout de même par <table> :
const txtDatasViewsElt=document.getElementById("datas").innerHTML;
expect(txtDatasViewsElt.indexOf("<table>")).toEqual(0);
// Et on doit retrouver le bon nombre de lignes :
const getTR=document.getElementsByTagName("tr");
expect(getTR.length).toEqual(119);
});
it("Si demandé, doit afficher le nombre de lignes de données du fichier.", async () =>
{
converter.datasViewElt={ id:"datas" };
converter.datasCounter={ id:"counter" };
await converter.run();
let txtDatasViewsElt=document.getElementById("counter").innerHTML;
expect(txtDatasViewsElt).toEqual("118");
});
});
/* describe("Action des champs de classement en corrélation avec le convertisseur.", () =>
{
it("Le 1er clic sur l'entête d'une des colonnes doit classer les données dans le sens ascendant, puis descendant et ainsi de suite.", async () =>
{
let sortingField=new SortingField(converter, 2);
sortingField.field2HTML();
// Compliqué de tester avec spyOn que sort() a été appelée avec la bonne fonction de classement en paramètre
// Donc je compare les résultats à ceux attendus
const sortingField=new SortingField(converter, 0);
converter.datasSortingFields=[sortingField];
let getTHLink=document.querySelector("th a") as HTMLElement;
getTHLink.click();// tri ascendant
let getTR=document.querySelectorAll("tr");
let txtDatasViewsElt=getTR[1].innerHTML;
expect(txtDatasViewsElt).toEqual("<td>89</td><td>Actinium</td><td>Ac</td><td>Actinide</td><td>≤ 1</td>");
getTHLink.click();// tri descendant
getTR=document.querySelectorAll("tr");
txtDatasViewsElt=getTR[1].innerHTML;
expect(txtDatasViewsElt).toEqual("<td>40</td><td>Zirconium</td><td>Zr</td><td>Métal de transition</td><td>&gt; 100000</td>");
getTHLink.click();// de nouveau ascendant
getTR=document.querySelectorAll("tr");
txtDatasViewsElt=getTR[1].innerHTML;
expect(txtDatasViewsElt).toEqual("<td>89</td><td>Actinium</td><td>Ac</td><td>Actinide</td><td>≤ 1</td>");
sortingField.field2HTML();
const fieldName=converter.fields[0];
const getTHLink=document.querySelector("th a") as HTMLElement;
getTHLink.click();
converter.datas.sort( (a, b) => compare( {order: "asc"} )(a[fieldName], b[fieldName]));
expect(converter.datas2Rend).toEqual(converter.datas);
getTHLink.click();
converter.datas.sort( (a, b) => compare( {order: "desc"} )(a[fieldName], b[fieldName]));
expect(converter.datas2Rend).toEqual(converter.datas);
getTHLink.click();
converter.datas.sort( (a, b) => compare( {order: "asc"} )(a[fieldName], b[fieldName]));
expect(converter.datas2Rend).toEqual(converter.datas);
});
it("Prise en compte d'une fonction spécifique associée au champ de classement.", async () =>
it("Si une fonction de classement est définie pour le champ activé par l'utilisateur, elle doit être prise en compte.", () =>
{
const mySort=(a: any, b: any, order: "asc"|"desc"="asc") =>
const mySort = (a: any, b: any, order: "asc"|"desc" = "asc") =>
{
const values=[ "> 100000", "> 1 et < 100 000", "≤ 1", "Traces", "Inexistant"];
const values = [ "> 100000", "> 1 et < 100 000", "≤ 1", "Traces", "Inexistant"];
if(order === "desc")
values.reverse();
if(values.indexOf(a) > values.indexOf(b))
@ -205,111 +306,74 @@ describe("Test du script central de FreeDatas2HTML", () =>
else
return 0;
};
converter.datasSortingFunctions=[{ datasFieldNb: 4, sort:mySort }];
let sortingField=new SortingField(converter, 4);
sortingField.field2HTML();
converter.datasSortingFunctions=[{ datasFieldNb:4, sort:mySort }];
const sortingField=new SortingField(converter, 4);
converter.datasSortingFields=[sortingField];
let getTHLink=document.querySelector("th a") as HTMLElement;
getTHLink.click();// tri ascendant
let getTR=document.querySelectorAll("tr");
let txtDatasViewsElt=getTR[1].innerHTML;
expect(txtDatasViewsElt).toEqual("<td>95</td><td>Américium</td><td>Am</td><td>Actinide</td><td>Inexistant</td>");
getTHLink.click();// tri descendant
getTR=document.querySelectorAll("tr");
txtDatasViewsElt=getTR[1].innerHTML;
expect(txtDatasViewsElt).toEqual("<td>1</td><td>Hydrogène</td><td>H</td><td>Non-métal</td><td>&gt; 100000</td>");
getTHLink.click();// de nouveau ascendant
getTR=document.querySelectorAll("tr");
txtDatasViewsElt=getTR[1].innerHTML;
expect(txtDatasViewsElt).toEqual("<td>95</td><td>Américium</td><td>Am</td><td>Actinide</td><td>Inexistant</td>");
sortingField.field2HTML();
const fieldName=converter.fields[0];
const getTHLink=document.querySelector("th a") as HTMLElement;
getTHLink.click();
converter.datas.sort( (a, b) => { return mySort(a[fieldName], b[fieldName], "asc"); });
expect(converter.datas2Rend).toEqual(converter.datas);
getTHLink.click();
converter.datas.sort( (a, b) => { return mySort(a[fieldName], b[fieldName], "desc"); });
expect(converter.datas2Rend).toEqual(converter.datas);
getTHLink.click();
converter.datas.sort( (a, b) => { return mySort(a[fieldName], b[fieldName], "asc"); });
expect(converter.datas2Rend).toEqual(converter.datas);
});
}); */
/*
describe("Création et action des sélecteurs liés à la pagination des données.", () =>
it("Si des options de pagination sont activées par l'utilisateur, seules les données de la page choisie doivent être retournées.", () =>
{
beforeEach( () =>
{
pagination.options={ displayElement: { id:"paginationOptions" }, values: [10,20,50,500] , name: "Choix de pagination :" };
const pagination=new Pagination(converter, { id:"pages" }, "Page à afficher :");
pagination.options={ displayElement: { id:"paginationOptions" }, values: [10,20,50] , name: "Choix de pagination :" };
pagination.selectedValue=10;
pagination.options2HTML();
converter.pagination=pagination;
//converter.refreshView();
pagination.options2HTML();
converter.refreshView(); // il ne doit plus rester que les 10 premiers enregistrement
expect(converter.datas2Rend).toEqual(converter.datas.slice(0,10));
// Sélection de la dernière page, avec une pagination à 50 :
const selectPagination=document.getElementById("freeDatas2HTMLPaginationSelector") as HTMLInputElement;
selectPagination.value="3";
selectPagination.dispatchEvent(new Event('change'));
const selectPage=document.getElementById("freeDatas2HTMLPagesSelector") as HTMLInputElement;
selectPage.value="3";
selectPage.dispatchEvent(new Event('change'));
expect(converter.datas2Rend).toEqual(converter.datas.slice(100));
// Annulation de la pagination. Affiche toutes les données :
selectPagination.value="0";
selectPagination.dispatchEvent(new Event('change'));
expect(converter.datas2Rend).toEqual(converter.datas);
});
it("Si une valeur de pagination par défaut fournie, ne doit pas afficher plus de données.", () =>
it("Si des filtres sont déclarés, ils doivent tous être appelés pour tester les données à afficher.", () =>
{
let getTR=document.getElementsByTagName("tr");
expect(getTR.length).toEqual(pagination.selectedValue+1); // 1er TR sert aux titres
const filter1=new Selector(converter, 3, { id:"selector1"} );
filter1.filter2HTML();
const filter2=new Selector(converter, 4, { id:"selector2"} );
converter.datasFilters=[filter1,filter2];
// si le 1er n'est pas réellement lancé, le second est bloqué, car cela retourne un "false"
spyOn(filter1, "dataIsOk").and.callThrough();
spyOn(filter2, "dataIsOk");
converter.refreshView();
expect(filter1.dataIsOk).toHaveBeenCalledTimes(118);
expect(filter2.dataIsOk).toHaveBeenCalledTimes(118);
});
it("La manipulation du sélecteur de pagination doit appeler la fonction actualisant l'affichage.", () =>
it("Quand il y a plusieurs filtres, seules les données positives aux précédents sont testées par les suivants.", () =>
{
spyOn(converter, "refreshView");
let selectElement=document.getElementById("freeDatas2HTMLPaginationSelector") as HTMLInputElement;
selectElement.value="2";
const filter1=new Selector(converter, 3, { id:"selector1"} );
filter1.filter2HTML();
const filter2=new Selector(converter, 4, { id:"selector2"} );
converter.datasFilters=[filter1,filter2];
const selectElement=document.getElementById("freeDatas2HTML_selector1") as HTMLInputElement;
selectElement.value="2"; // correspond à 4 enregistrements
spyOn(filter1, "dataIsOk").and.callThrough();
spyOn(filter2, "dataIsOk");
// Doit vraiment être lancé pour que la valeur sélectionnée soit retenue pour filter les données
selectElement.dispatchEvent(new Event('change'));
expect(converter.refreshView).toHaveBeenCalledTimes(1);
selectElement.value="0";
selectElement.dispatchEvent(new Event('change'));
expect(converter.refreshView).toHaveBeenCalledTimes(2);
});
it("Si une des options de pagination fournies est sélectionnée, doit afficher le nombre de résultats correspondants.", () =>
{
let selectElement=document.getElementById("freeDatas2HTMLPaginationSelector") as HTMLInputElement;
selectElement.value="2"; // = 20 éléments / page
selectElement.dispatchEvent(new Event('change'));
let getTR=document.getElementsByTagName("tr");
expect(getTR.length).toEqual(21);
selectElement.value="3"; // = 50 éléments / page
selectElement.dispatchEvent(new Event('change'));
getTR=document.getElementsByTagName("tr");
expect(getTR.length).toEqual(51);
selectElement.value="0"; // = pas de Pagination, on affiche les 118 lignes du fichier
selectElement.dispatchEvent(new Event('change'));
getTR=document.getElementsByTagName("tr");
expect(getTR.length).toEqual(119);
});
it("Si il y a plus de données que le nombre de lignes autorisées par page, un <select> listant les pages doit être affiché.", () =>
{
let selectElement=document.getElementById("pages").innerHTML;
expect(selectElement).toEqual(fixtures.selectorForPages);
});
it("La manipulation du sélecteur de pages doit appeler la fonction actualisant l'affichage.", () =>
{
spyOn(converter, "refreshView");
let selectElement=document.getElementById("freeDatas2HTMLPagesSelector") as HTMLInputElement;
selectElement.value="2";
selectElement.dispatchEvent(new Event('change'));
expect(converter.refreshView).toHaveBeenCalledTimes(1);
selectElement.value="0";
selectElement.dispatchEvent(new Event('change'));
expect(converter.refreshView).toHaveBeenCalledTimes(2);
});
it("Si l'utilisateur sélectionne une des pages proposées, l'affichage des résultats doit s'adapter en prenant en compte la pagination sélectionnée.", () =>
{
let selectElement=document.getElementById("freeDatas2HTMLPaginationSelector") as HTMLInputElement;
selectElement.value="3"; // = 50 éléments / page
selectElement.dispatchEvent(new Event('change'));
selectElement=document.getElementById("freeDatas2HTMLPagesSelector") as HTMLInputElement;
selectElement.value="2";
selectElement.dispatchEvent(new Event('change'));
let getTR=document.getElementsByTagName("tr");
expect(getTR[1].innerHTML).toEqual(fixtures.firstLineForPageSelection1);
expect(getTR[50].innerHTML).toEqual(fixtures.lastLineForPageSelection1);
selectElement.value="3"; // troisième page = incomplète (18 enregistrements)
selectElement.dispatchEvent(new Event('change'));
getTR=document.getElementsByTagName("tr");
expect(getTR[1].innerHTML).toEqual(fixtures.firstLineForPageSelection2);
expect(getTR[18].innerHTML).toEqual(fixtures.lastLineForPageSelection2);
expect(getTR[50]).toBeUndefined();
expect(filter1.dataIsOk).toHaveBeenCalledTimes(118);
expect(filter2.dataIsOk).toHaveBeenCalledTimes(4);
});
});
});*/
});