From 739b83f36575fa8a39142fff08b1cfd7eff68231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabrice=20PENHO=C3=8BT?= Date: Tue, 26 Oct 2021 18:01:23 +0200 Subject: [PATCH] =?UTF-8?q?Version=20remani=C3=A9e=20de=20la=20classe=20pr?= =?UTF-8?q?incipale=20+=20r=C3=A9=C3=A9criture=20de=20ses=20tests.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- src/FreeDatas2HTML.ts | 229 ++++++++--------- src/errors.js | 2 +- src/interfaces.ts | 7 +- tests/fixtures.js | 1 - tests/freeDatas2HTMLSpec.ts | 492 ++++++++++++++++++++---------------- 6 files changed, 393 insertions(+), 340 deletions(-) diff --git a/package.json b/package.json index f0f9481..a31ac3e 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/FreeDatas2HTML.ts b/src/FreeDatas2HTML.ts index 4b3197a..5c06d94 100644 --- a/src/FreeDatas2HTML.ts +++ b/src/FreeDatas2HTML.ts @@ -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; + + // 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 résultats : + private _fields: ParseResults["fields"]=[]; + private _datas: ParseResults["datas"]=[]; + private _datas2Rend: {[index: string]:string}[]=[]; + private _nbDatasValid: number=0; - // 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 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 = {}; - - // 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); @@ -79,24 +67,49 @@ export class FreeDatas2HTML return checkedElt; } } - - // 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=[]; @@ -108,22 +121,7 @@ export class FreeDatas2HTML this._datasSortingFunctions.push(checkedFunction); } } - - // 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 { 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]); - i++; - } + 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"; diff --git a/src/errors.js b/src/errors.js index e4a695d..cb195ea 100644 --- a/src/errors.js +++ b/src/errors.js @@ -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.", diff --git a/src/interfaces.ts b/src/interfaces.ts index 4fec8a4..392fcc4 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -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 { diff --git a/tests/fixtures.js b/tests/fixtures.js index 8a2d594..ec95f8a 100644 --- a/tests/fixtures.js +++ b/tests/fixtures.js @@ -11,7 +11,6 @@ module.exports = sortingColumn1HTML: 'Z (numéro atomique)', sortingColumn2HTML: 'Symbole', selectorForPagination: '', - selectorFor2Pages: '', selectorForManyPages: '', firstLineForPageSelection1:"51AntimoineSbMétalloïde> 1 et < 100 000", lastLineForPageSelection1:"100FermiumFmActinideInexistant", diff --git a/tests/freeDatas2HTMLSpec.ts b/tests/freeDatas2HTMLSpec.ts index 653b44b..7ca4f90 100644 --- a/tests/freeDatas2HTMLSpec.ts +++ b/tests/freeDatas2HTMLSpec.ts @@ -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.", () => + { + beforeEach( async () => { - let check=converter.checkFieldExist(2); // aucune donnée chargée, donc le champ ne peut être trouvé - expect(check).toBeFalse(); + 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 () => - { - 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 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(); }).not.toThrowError(); + expect(() => { return converter.refreshView(); }).toThrowError(errors.converterRefreshFail); + }); + + 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("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 des champs de classement existent, leur code HTML doit être actualisé.", () => { - 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)); + 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 cela n'est pas demandé, le script ne sera pas bloqué, même si des erreurs sont rencontrées durant le parsage.", async () => + it("Si une pagination est configurée, le code HTML listant les pages doit être actualisé.", () => { - 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 ">") - expect(converter.datasHTML).toEqual(htmlForDatas); - // Mais le code commence tout de même par : - const txtDatasViewsElt=document.getElementById("datas").innerHTML; - expect(txtDatasViewsElt.indexOf("
")).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"); + const pagination=new Pagination(converter, { id:"pages" }, "Page à afficher :"); + converter.pagination=pagination; + spyOn(pagination, "pages2HTML"); + converter.refreshView(); + expect(pagination.pages2HTML).toHaveBeenCalled(); }); }); - /* describe("Action des champs de classement en corrélation avec le convertisseur.", () => + describe("Fonction filtrant les données à afficher.", () => { - - 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 () => + beforeEach( async () => { - let sortingField=new SortingField(converter, 2); - sortingField.field2HTML(); + converter.parser.setRemoteSource({ url:"http://localhost:9876/datas/datas1.csv" }); + converter.datasViewElt={ id:"datas" }; + await converter.run(); + }); + + it("Si un champ de classement est activé par l'utilisateur, les données doivent être classées via ce champ.", () => + { + // 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(""); - getTHLink.click();// tri descendant - getTR=document.querySelectorAll("tr"); - txtDatasViewsElt=getTR[1].innerHTML; - expect(txtDatasViewsElt).toEqual(""); - getTHLink.click();// de nouveau ascendant - getTR=document.querySelectorAll("tr"); - txtDatasViewsElt=getTR[1].innerHTML; - expect(txtDatasViewsElt).toEqual(""); + 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(""); - getTHLink.click();// tri descendant - getTR=document.querySelectorAll("tr"); - txtDatasViewsElt=getTR[1].innerHTML; - expect(txtDatasViewsElt).toEqual(""); - getTHLink.click();// de nouveau ascendant - getTR=document.querySelectorAll("tr"); - txtDatasViewsElt=getTR[1].innerHTML; - expect(txtDatasViewsElt).toEqual(""); - }); + 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.", () => - { - beforeEach( () => + it("Si des options de pagination sont activées par l'utilisateur, seules les données de la page choisie doivent être retournées.", () => { - 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 - }); - - it("La manipulation du sélecteur de pagination doit appeler la fonction actualisant l'affichage.", () => - { - spyOn(converter, "refreshView"); - let selectElement=document.getElementById("freeDatas2HTMLPaginationSelector") 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); + 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("Si une des options de pagination fournies est sélectionnée, doit afficher le nombre de résultats correspondants.", () => + it("Quand il y a plusieurs filtres, seules les données positives aux précédents sont testées par les suivants.", () => { - let selectElement=document.getElementById("freeDatas2HTMLPaginationSelector") as HTMLInputElement; - selectElement.value="2"; // = 20 éléments / page + 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')); - 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
89ActiniumAcActinide≤ 140ZirconiumZrMétal de transition> 10000089ActiniumAcActinide≤ 195AmériciumAmActinideInexistant1HydrogèneHNon-métal> 10000095AmériciumAmActinideInexistant