Bladeren bron

Better tests for OSMD; Support for Promises in loading sheet music

Andrea Condoluci 9 jaren geleden
bovenliggende
commit
f8e2269419

+ 13 - 3
src/Common/FileIO/Mxl.ts

@@ -12,7 +12,7 @@ import JSZip = require("jszip");
 //     // Handle it here.
 //   }
 // )
-export function extractSheetFromMxl(data: string): Promise<any> {
+export function MXLtoIXmlElement(data: string): Promise<IXmlElement> {
   "use strict";
   let zip: JSZip.JSZip = new JSZip();
   // asynchronously load zip file and process it - with Promises
@@ -45,7 +45,7 @@ export function extractSheetFromMxl(data: string): Promise<any> {
     }
   ).then(
     (content: IXmlElement) => {
-      return Promise.resolve(content);
+      return content;
     },
     (err: any) => {
       throw new Error("extractSheetFromMxl: " + err.message);
@@ -53,7 +53,7 @@ export function extractSheetFromMxl(data: string): Promise<any> {
   );
 }
 
-export function openMxl(data: string): Promise<any> {
+export function MXLtoXMLstring(data: string): Promise<string> {
     "use strict";
     let zip: JSZip.JSZip = new JSZip();
     // asynchronously load zip file and process it - with Promises
@@ -64,5 +64,15 @@ export function openMxl(data: string): Promise<any> {
         (err: any) => {
             throw err;
         }
+    ).then(
+        (content: string) => {
+            let parser: DOMParser = new DOMParser();
+            let doc: Document = parser.parseFromString(content, "text/xml");
+            let rootFile: string = doc.getElementsByTagName("rootfile")[0].getAttribute("full-path");
+            return zip.file(rootFile).async("string");
+        },
+        (err: any) => {
+            throw err;
+        }
     );
 }

+ 13 - 8
src/Common/FileIO/Xml.ts

@@ -11,6 +11,9 @@ export class IXmlElement {
     private elem: Element;
 
     constructor(elem: Element) {
+        if (elem === undefined) {
+            throw new Error("IXmlElement: expected Element, got undefined");
+        }
         this.elem = elem;
         this.name = elem.nodeName.toLowerCase();
 
@@ -32,7 +35,7 @@ export class IXmlElement {
     }
 
     public attributes(): IXmlAttribute[] {
-        if (typeof this.attrs === "undefined") {
+        if (!this.attrs) {
             let attributes: NamedNodeMap = this.elem.attributes;
             let attrs: IXmlAttribute[] = [];
             for (let i: number = 0; i < attributes.length; i += 1) {
@@ -44,7 +47,13 @@ export class IXmlElement {
     }
 
     public element(elementName: string): IXmlElement {
-        return this.elements(elementName)[0];
+        let nodes: NodeList = this.elem.childNodes;
+        for (let i: number = 0, length: number = nodes.length; i < length; i += 1) {
+            let node: Node = nodes[i];
+            if (node.nodeType === Node.ELEMENT_NODE && node.nodeName.toLowerCase() === elementName) {
+                return new IXmlElement(node as Element);
+            }
+        }
     }
 
     public elements(nodeName?: string): IXmlElement[] {
@@ -54,15 +63,11 @@ export class IXmlElement {
         if (!nameUnset) {
             nodeName = nodeName.toLowerCase();
         }
-        // console.log("-", nodeName, nodes.length, this.elem.childElementCount, this.elem.getElementsByTagName(nodeName).length);
-        // if (nodeName === "measure") {
-        //   console.log(this.elem);
-        // }
         for (let i: number = 0; i < nodes.length; i += 1) {
             let node: Node = nodes[i];
-            //console.log("node: ", this.elem.nodeName, ">>", node.nodeName, node.nodeType === Node.ELEMENT_NODE);
             if (node.nodeType === Node.ELEMENT_NODE &&
-                (nameUnset || node.nodeName.toLowerCase() === nodeName)) {
+                (nameUnset || node.nodeName.toLowerCase() === nodeName)
+            ) {
                 ret.push(new IXmlElement(node as Element));
             }
         }

+ 34 - 19
src/OSMD/OSMD.ts

@@ -6,8 +6,8 @@ import {MusicSheetCalculator} from "./../MusicalScore/Graphical/MusicSheetCalcul
 import {VexFlowMusicSheetDrawer} from "./../MusicalScore/Graphical/VexFlow/VexFlowMusicSheetDrawer";
 import {MusicSheet} from "./../MusicalScore/MusicSheet";
 import {Cursor} from "./Cursor";
-import {openMxl} from "../Common/FileIO/Mxl";
-//import {Promise} from "es6-promise";
+import {MXLtoXMLstring} from "../Common/FileIO/Mxl";
+import {Promise} from "es6-promise";
 import {handleResize} from "./ResizeHandler";
 
 export class OSMD {
@@ -60,7 +60,7 @@ export class OSMD {
      * Load a MusicXML file
      * @param content is either the url of a file, or the root node of a MusicXML document, or the string content of a .xml/.mxl file
      */
-    public load(content: string|Document): void {
+    public load(content: string|Document): Promise<{}> {
         // Warning! This function is asynchronous! No error handling is done here.
         // FIXME TODO Refactor with Promises
         this.reset();
@@ -70,18 +70,22 @@ export class OSMD {
             if (str.substr(0, 4) === "http") {
                 // Retrieve the file at the url
                 path = str;
-                this.openURL(path);
-                return;
+                return this.openURL(path).then(
+                    (s: string) => { return {}; },
+                    (exc: Error) => { throw exc; }
+                );
             }
-            if (str.substr(0, 4) === "\x04\x03\x4b\x50") {
+            if (str.substr(0, 4) === "\x50\x4b\x03\x04") {
                 // This is a zip file, unpack it first
-                openMxl(str).then(
-                    this.load,
+                let self: OSMD = this;
+                return MXLtoXMLstring(str).then(
+                    (str: string) => {
+                        return self.load(str);
+                    },
                     (err: any) => {
                         throw new Error("OSMD: Invalid MXL file");
                     }
                 );
-                return;
             }
             if (str.substr(0, 5) === "<?xml") {
                 // Parse the string representing an xml file
@@ -91,11 +95,19 @@ export class OSMD {
         }
 
         if (!content || !(<any>content).nodeName) {
-            throw new Error("OSMD: Document provided is not valid");
+            return Promise.reject(new Error("OSMD: Document provided is not valid"));
+        }
+        let children: NodeList = (<Document>content).childNodes;
+        let elem: Element;
+        for (let i: number = 0, length: number = children.length; i < length; i += 1) {
+            let node: Node = children[i];
+            if (node.nodeType === Node.ELEMENT_NODE && node.nodeName.toLowerCase() === "score-partwise") {
+                elem = <Element>node;
+                break;
+            }
         }
-        let elem: Element = (<Document>content).getElementsByTagName("score-partwise")[0];
-        if (elem === undefined) {
-            throw new Error("OSMD: Document is not valid partwise MusicXML");
+        if (!elem) {
+            return Promise.reject(new Error("OSMD: Document is not a valid 'partwise' MusicXML"));
         }
         let score: IXmlElement = new IXmlElement(elem);
         let calc: MusicSheetCalculator = new VexFlowMusicSheetCalculator();
@@ -103,7 +115,7 @@ export class OSMD {
         this.sheet = reader.createMusicSheet(score, path);
         this.graphic = new GraphicalMusicSheet(this.sheet, calc);
         this.cursor.init(this.sheet.MusicPartManager, this.graphic);
-        return; // Promise.resolve();
+        return Promise.resolve({});
     }
 
     /**
@@ -115,9 +127,12 @@ export class OSMD {
             throw new Error("OSMD: Before rendering a music sheet, please load a MusicXML file");
         }
         let width: number = this.container.offsetWidth;
-        if (isNaN(width)) {
-            throw new Error("OSMD: Before rendering a music sheet, please give the container a width");
-        }
+        // Before introducing the following optimization (maybe irrelevant), tests
+        // have to be modified to ensure that width is > 0 when executed
+        //if (isNaN(width) || width === 0) {
+        //    return;
+        //}
+
         // Set page width
         this.sheet.pageWidth = width / this.zoom / 10.0;
         // Calculate again
@@ -136,8 +151,8 @@ export class OSMD {
      *
      * @param url
      */
-    private openURL(url: string): string {
-        throw new Error("OSMD: Not implemented: Load sheet from URL");
+    private openURL(url: string): Promise<string> {
+        return Promise.reject(new Error("OSMD: Not implemented: Load sheet from URL"));
         //let JSZipUtils: any;
         //let self: OSMD = this;
         //JSZipUtils.getBinaryContent(url, function (err, data) {

+ 5 - 11
test/Common/FileIO/Mxl.ts

@@ -1,25 +1,20 @@
 import { IXmlElement } from "../../../src/Common/FileIO/Xml";
-import { extractSheetFromMxl } from "../../../src/Common/FileIO/Mxl.ts";
-
+import { MXLtoIXmlElement } from "../../../src/Common/FileIO/Mxl.ts";
+import { TestUtils } from "../../Util/TestUtils";
 
 describe("MXL Tests", () => {
-  // Load the mxl file
-  function getSheet(filename: string): string {
-    return ((window as any).__raw__)[filename];
-  }
-
   // Generates a test for a mxl file name
   function testFile(scoreName: string): void {
     it(scoreName, (done: MochaDone) => {
       // Load the xml file content
-      let mxl: string = getSheet("test/data/" + scoreName + ".mxl");
+      let mxl: string = TestUtils.getMXL(scoreName);
       chai.expect(mxl).to.not.be.undefined;
       // Extract XML from MXL
       // Warning: the sheet is loaded asynchronously,
       // (with Promises), thus we need a little fix
       // in the end with 'then(null, done)' to
       // make Mocha work asynchronously
-      extractSheetFromMxl(mxl).then(
+      MXLtoIXmlElement(mxl).then(
         (score: IXmlElement) => {
           chai.expect(score).to.not.be.undefined;
           chai.expect(score.name).to.equal("score-partwise");
@@ -38,7 +33,7 @@ describe("MXL Tests", () => {
 
   // Test failure
   it("Corrupted file", (done: MochaDone) => {
-    extractSheetFromMxl("").then(
+    MXLtoIXmlElement("").then(
       (score: IXmlElement) => {
         chai.expect(score).to.not.be.undefined;
         chai.expect(score.name).to.equal("score-partwise");
@@ -47,5 +42,4 @@ describe("MXL Tests", () => {
       (exc: any) => { done(); }
     );
   });
-
 });

+ 4 - 3
test/Common/FileIO/Xml.ts

@@ -8,12 +8,12 @@ let xmlTestData: string = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
 <credit-words justify=\"center\" valign=\"top\">Example Credit Words</credit-words> </credit>  </score-partwise>";
 
 
-describe("XML Unit Tests", () => {
+describe("XML interface", () => {
   let parser: DOMParser = new DOMParser();
   let doc: Document = parser.parseFromString(xmlTestData, "text/xml");
   let documentElement: IXmlElement = new IXmlElement(doc.documentElement);
 
-  it("IXmlElement Tests", (done: MochaDone) => {
+  it("test IXmlElement", (done: MochaDone) => {
     // Test name attribute
     chai.expect(documentElement.name).to.equal("score-partwise");
     // Test element method
@@ -25,7 +25,8 @@ describe("XML Unit Tests", () => {
       .element("software").value).to.equal("Example Software name");
     done();
   });
-  it("IXmlAttribute Tests", (done: MochaDone) => {
+
+  it("test IXmlAttribute", (done: MochaDone) => {
     // Test attributes method
     chai.expect(
       documentElement.element("credit").attributes()[0].name

+ 86 - 0
test/Common/OSMD/OSMD.ts

@@ -1,5 +1,6 @@
 import chai = require("chai");
 import {OSMD} from "../../../src/OSMD/OSMD";
+import {TestUtils} from "../../Util/TestUtils";
 
 
 describe("OSMD Main Export", () => {
@@ -19,4 +20,89 @@ describe("OSMD Main Export", () => {
         done();
     });
 
+    it("load MXL from string", (done: MochaDone) => {
+        let mxl: string = TestUtils.getMXL("MozartTrio");
+        let div: HTMLElement = document.createElement("div");
+        let osmd: OSMD = new OSMD(div);
+        osmd.load(mxl).then(
+            (_: {}) => {
+                osmd.render();
+                done();
+            },
+            done
+        );
+    });
+
+    it("load invalid MXL from string", (done: MochaDone) => {
+        let mxl: string = "\x50\x4b\x03\x04";
+        let div: HTMLElement = document.createElement("div");
+        let osmd: OSMD = new OSMD(div);
+        osmd.load(mxl).then(
+            (_: {}) => {
+                done(new Error("Corrupted MXL appears to be loaded correctly"));
+            },
+            (exc: Error) => {
+                if (exc.message.toLowerCase().match(/invalid/)) {
+                    done();
+                } else {
+                    done(new Error("Unexpected error: " + exc.message));
+                }
+            }
+        );
+    });
+
+    it("load XML string", (done: MochaDone) => {
+        let score: Document = TestUtils.getScore("MuzioClementi_SonatinaOpus36No1_Part1");
+        let xml: string = new XMLSerializer().serializeToString(score);
+        let div: HTMLElement = document.createElement("div");
+        let osmd: OSMD = new OSMD(div);
+        osmd.load(xml).then(
+            (_: {}) => {
+                osmd.render();
+                done();
+            },
+            done
+        );
+    });
+
+    it("load XML Document", (done: MochaDone) => {
+        let score: Document = TestUtils.getScore("MuzioClementi_SonatinaOpus36No1_Part1");
+        let div: HTMLElement = document.createElement("div");
+        let osmd: OSMD = new OSMD(div);
+        osmd.load(score).then(
+            (_: {}) => {
+                osmd.render();
+                done();
+            },
+            done
+        );
+    });
+
+    it("load invalid XML string", (done: MochaDone) => {
+        let xml: string = "<?xml";
+        let div: HTMLElement = document.createElement("div");
+        let osmd: OSMD = new OSMD(div);
+        osmd.load(xml).then(
+            (_: {}) => {
+                done(new Error("Corrupted XML appears to be loaded correctly"));
+            },
+            (exc: Error) => {
+                if (exc.message.toLowerCase().match(/partwise/)) {
+                    done();
+                } else {
+                    done(new Error("Unexpected error: " + exc.message));
+                }
+            }
+        );
+    });
+
+    it("render without loading", (done: MochaDone) => {
+        let div: HTMLElement = document.createElement("div");
+        let osmd: OSMD = new OSMD(div);
+        chai.expect(() => {
+            return osmd.render();
+        }).to.throw(/load/);
+        done();
+    });
+
 });

+ 4 - 4
test/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetDrawer.ts

@@ -11,13 +11,13 @@ import {OutlineAndFillStyleEnum} from "../../../../src/MusicalScore/Graphical/Dr
 describe("VexFlow Music Sheet Drawer", () => {
 
     it(".drawSheet (Clementi pt. 1)", (done: MochaDone) => {
-        let path: string = "test/data/MuzioClementi_SonatinaOpus36No1_Part2.xml";
-        // "test/data/MuzioClementi_SonatinaOpus36No1_Part1.xml";
-        let score: IXmlElement = TestUtils.getScore(path);
+        let score: Document = TestUtils.getScore("MuzioClementi_SonatinaOpus36No1_Part1");
         chai.expect(score).to.not.be.undefined;
+        let partwise: Element = TestUtils.getPartWiseElement(score);
+        chai.expect(partwise).to.not.be.undefined;
         let calc: VexFlowMusicSheetCalculator = new VexFlowMusicSheetCalculator();
         let reader: MusicSheetReader = new MusicSheetReader();
-        let sheet: MusicSheet = reader.createMusicSheet(score, path);
+        let sheet: MusicSheet = reader.createMusicSheet(new IXmlElement(partwise), "path");
         let gms: GraphicalMusicSheet = new GraphicalMusicSheet(sheet, calc);
         gms.Cursors.push(gms.calculateCursorLineAtTimestamp(new Fraction(), OutlineAndFillStyleEnum.PlaybackCursor));
 

+ 6 - 4
test/MusicalScore/ScoreCalculation/MusicSheetCalculator_Test.ts

@@ -13,7 +13,7 @@ import {TestUtils} from "../../Util/TestUtils";
 
 describe("Music Sheet Calculator Tests", () => {
     // Initialize variables
-    let path: string = "test/data/MuzioClementi_SonatinaOpus36No1_Part1.xml";
+    let filename: string = "MuzioClementi_SonatinaOpus36No1_Part1";
     let reader: MusicSheetReader = new MusicSheetReader();
     let calculator: MusicSheetCalculator = new VexFlowMusicSheetCalculator();
     let score: IXmlElement;
@@ -33,10 +33,12 @@ describe("Music Sheet Calculator Tests", () => {
 
     it("Do Calculation", (done: MochaDone) => {
         MusicSheetCalculator.TextMeasurer = new VexFlowTextMeasurer();
-        // Load the xml file
-        score = TestUtils.getScore(path);
+        // Load the XML file
+        let xml: Document = TestUtils.getScore(filename);
+        chai.expect(xml).to.not.be.undefined;
+        score = new IXmlElement(TestUtils.getPartWiseElement(xml));
         chai.expect(score).to.not.be.undefined;
-        sheet = reader.createMusicSheet(score, path);
+        sheet = reader.createMusicSheet(score, "path-of-" + filename);
 
         let graphicalSheet: GraphicalMusicSheet = new GraphicalMusicSheet(sheet, calculator);
         graphicalSheet.reCalculate();

+ 29 - 11
test/Util/TestUtils.ts

@@ -1,15 +1,33 @@
-import {IXmlElement} from "../../src/Common/FileIO/Xml";
-
+/**
+ * This class collects useful methods to interact with test data.
+ * During tests, XML and MXL documents are preprocessed by karma,
+ * and this is some helper code to retrieve them.
+ */
 export class TestUtils {
-    public static getScore(path: string): IXmlElement {
-        let doc: Document = ((window as any).__xml__)[path];
-        if (doc === undefined) {
-            return;
-        }
-        let elem: Element = doc.getElementsByTagName("score-partwise")[0];
-        if (elem === undefined) {
-            return;
+
+    public static getScore(name: string): Document {
+        let path: string = "test/data/" + name + ".xml";
+        return ((window as any).__xml__)[path];
+    }
+
+    public static getMXL(scoreName: string): string {
+        let path: string = "test/data/" + scoreName + ".mxl";
+        return ((window as any).__raw__)[path];
+    }
+
+    /**
+     * Retrieve from a XML document the first element with name "score-partwise"
+     * @param doc is the XML Document
+     * @returns {Element}
+     */
+    public static getPartWiseElement(doc: Document): Element {
+        let nodes: NodeList = doc.childNodes;
+        for (let i: number = 0, length: number = nodes.length; i < length; i += 1) {
+            let node: Node = nodes[i];
+            if (node.nodeType === Node.ELEMENT_NODE && node.nodeName.toLowerCase() === "score-partwise") {
+                return <Element>node;
+            }
         }
-        return new IXmlElement(elem);
     }
+
 }