<|start_filename|>test/translate-jest-matchers.js<|end_filename|>
import { toMatchSnapshot } from "jest-snapshot";

// webpack uses special errors that store the original error message
// in error.error.message. This serilaizer unwraps the webpack errors.
expect.addSnapshotSerializer({
  test(value) {
    return value instanceof Error && typeof value.error === "object";
  },
  print(value, serialize) {
    return `${value.name}: ${serialize(value.error)}`;
  }
});

// Matches the empty error where we need to remove the absolute paths
expect.addSnapshotSerializer({
  test(value) {
    return value instanceof Error && typeof value.error === "object";
  },
  print(value, serialize) {
    return `${value.name}: ${serialize(value.error.message)}`;
  }
});

expect.extend({
  toHaveEmittedErrorMatchingSnapshot(loaderContext) {
    return toMatchSnapshot.call(this, loaderContext.emitError.mock.calls);
  }
});


<|start_filename|>test/translations-registry.spec.js<|end_filename|>
import Translation from "../src/translation";
import TranslationsRegistry from "../dist/translations-registry";

describe("TranslationsRegistry", function() {
  "use strict";
  var registry;
  var RESOURCE = "test.js";

  beforeEach(function() {
    registry = new TranslationsRegistry();
  });

  describe("empty", function() {
    it("is true by default", function() {
      expect(registry.empty).toBe(true);
    });

    it("is false if a translation is registered", function() {
      registry.registerTranslation(createTranslation("test"));

      expect(registry.empty).toBe(false);
    });
  });

  describe("toJSON", function() {
    it("returns an empty object by default", function() {
      expect(registry.toJSON()).toEqual({});
    });

    it("returns the registered translation", function() {
      registry.registerTranslation(createTranslation("test", "Test"));

      expect(registry.toJSON()).toEqual({ test: "Test" });
    });

    it("returns all registered translations", function() {
      registry.registerTranslation(createTranslation("test", "Test"));
      registry.registerTranslation(createTranslation("other"));

      expect(registry.toJSON()).toEqual({ test: "Test", other: "other" });
    });
  });

  describe("registerTranslation", function() {
    it("merges translations with the same translation id", function() {
      registry.registerTranslation(createTranslation("test", "Test", 1, 1));
      registry.registerTranslation(createTranslation("test", "Test", 10, 1));

      var translation = registry.getTranslation("test");
      expect(translation).toBeTruthy();

      expect(translation.usages).toEqual([
        {
          resource: RESOURCE,
          loc: {
            line: 10,
            column: 1
          }
        },
        {
          resource: RESOURCE,
          loc: {
            line: 1,
            column: 1
          }
        }
      ]);

      expect(registry.toJSON()).toEqual({ test: "Test" });
    });

    it("throws if another translation with a different default text exists", function() {
      registry.registerTranslation(createTranslation("test", "Test"));

      expect(function() {
        registry.registerTranslation(
          createTranslation("test", "Other default text", 10)
        );
      }).toThrowErrorMatchingInlineSnapshot(`
"Webpack-Angular-Translate: Two translations with the same id but different default text found.
	Existing: {
  \\"id\\": \\"test\\",
  \\"defaultText\\": \\"Test\\",
  \\"usages\\": [
    \\"test.js:1:1\\"
  ]
}
	New: {
  \\"id\\": \\"test\\",
  \\"defaultText\\": \\"Other default text\\",
  \\"usages\\": [
    \\"test.js:10:1\\"
  ]
}
	Please define the same default text twice or specify the default text only once."
`);
    });

    it("throws if the translation id is an empty string", function() {
      expect(function() {
        registry.registerTranslation(createTranslation(""));
      }).toThrowErrorMatchingInlineSnapshot(`
"Invalid angular-translate translation found: The id of the translation is empty. Consider removing the translate attribute (html) or defining the translation id (js).
Translation:
'{
  \\"id\\": \\"\\",
  \\"defaultText\\": null,
  \\"usages\\": [
    \\"test.js:1:1\\"
  ]
}'"
`);
    });

    it("throws if the translation id is undefined", function() {
      expect(function() {
        registry.registerTranslation(createTranslation());
      }).toThrowErrorMatchingInlineSnapshot(`
"Invalid angular-translate translation found: The id of the translation is empty. Consider removing the translate attribute (html) or defining the translation id (js).
Translation:
'{
  \\"id\\": null,
  \\"defaultText\\": null,
  \\"usages\\": [
    \\"test.js:1:1\\"
  ]
}'"
`);
    });

    it("throws if the translation id is null", function() {
      expect(function() {
        registry.registerTranslation(createTranslation(null));
      }).toThrowErrorMatchingInlineSnapshot(`
"Invalid angular-translate translation found: The id of the translation is empty. Consider removing the translate attribute (html) or defining the translation id (js).
Translation:
'{
  \\"id\\": null,
  \\"defaultText\\": null,
  \\"usages\\": [
    \\"test.js:1:1\\"
  ]
}'"
`);
    });
  });

  describe("pruneTranslations", function() {
    it("removes all translations from the specified resource", function() {
      registry.registerTranslation(createTranslation("test", "Test"));
      registry.registerTranslation(createTranslation("other", "Other"));

      registry.pruneTranslations(RESOURCE);

      expect(registry.empty).toBe(true);
      expect(registry.toJSON()).toEqual({});
    });

    it("does not remove translations from other resources", function() {
      registry.registerTranslation(createTranslation("test", "Test"));
      registry.registerTranslation(
        new Translation("other", "Other", {
          resource: "other.js",
          loc: { line: 1, column: 1 }
        })
      );

      registry.pruneTranslations(RESOURCE);

      expect(registry.empty).toBe(false);
      expect(registry.toJSON()).toEqual({ other: "Other" });
    });

    it("does not remove translation if it is used by another resource", function() {
      registry.registerTranslation(createTranslation("test", "Test"));
      registry.registerTranslation(
        new Translation("test", "Test", {
          resource: "other.js",
          loc: { line: 1, column: 1 }
        })
      );

      registry.pruneTranslations(RESOURCE);

      expect(registry.empty).toBe(false);
      expect(registry.toJSON()).toEqual({ test: "Test" });
    });

    it("removes a translation if all usages have been pruned", function() {
      registry.registerTranslation(createTranslation("test", "Test"));
      registry.registerTranslation(
        new Translation("test", "Test", {
          resource: "other.js",
          loc: { line: 1, column: 1 }
        })
      );

      registry.pruneTranslations(RESOURCE);
      registry.pruneTranslations("other.js");

      expect(registry.empty).toBe(true);
      expect(registry.toJSON()).toEqual({});
    });
  });

  function createTranslation(id, defaultText, line, column) {
    return new Translation(id, defaultText, {
      resource: RESOURCE,
      loc: { line: line || 1, column: column || 1 }
    });
  }
});


<|start_filename|>test/js/translate-visitor.spec.js<|end_filename|>
import "../translate-jest-matchers";
import { builders as b, namedTypes as n, NodePath } from "ast-types";
import createTranslateVisitor, {
  isCommentedWithSuppressError,
} from "../../src/js/translate-visitor";

describe("TranslateVisitor", function () {
  let loaderContext;
  let visitor;

  beforeEach(() => {
    loaderContext = {
      registerTranslation: jest.fn().mockName("registerTranslation"),
      pruneTranslations: jest.fn(),
      emitError: jest.fn(),
      context: "path",
      resourcePath: "path/test.js",
      resource: "test.js",
    };

    visitor = createTranslateVisitor(loaderContext);
  });

  describe("$translate", () => {
    let $translate;

    beforeEach(() => {
      $translate = b.identifier("$translate");
    });

    it("extracts the translation with it's id from a $translate call with a single argument", () => {
      let translateCall = b.callExpression($translate, [b.literal("test")]);
      translateCall.loc = { start: { line: 1, column: 1 } };

      visitor.visit(translateCall);

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith({
        id: "test",
        defaultText: undefined,
        usages: [
          {
            resource: "test.js",
            loc: { line: 1, column: 1 },
          },
        ],
      });
    });

    it("extracts the translation with it's id and default text from a $translate call with a four argument", () => {
      let translateCall = b.callExpression($translate, [
        b.literal("test"),
        b.identifier("undefined"),
        b.identifier("undefined"),
        b.literal("Test"),
      ]);
      translateCall.loc = { start: { line: 1, column: 1 } };

      visitor.visit(translateCall);

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith({
        id: "test",
        defaultText: "Test",
        usages: [
          {
            resource: "test.js",
            loc: { line: 1, column: 1 },
          },
        ],
      });
    });

    it("extracts all translation with their ids for a $translate call with an array of translation ids", function () {
      let translateCall = b.callExpression($translate, [
        b.arrayExpression([b.literal("test"), b.literal("test2")]),
      ]);
      translateCall.loc = { start: { line: 1, column: 1 } };

      visitor.visit(translateCall);

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith({
        id: "test",
        defaultText: undefined,
        usages: [
          {
            resource: "test.js",
            loc: { line: 1, column: 1 },
          },
        ],
      });

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith({
        id: "test2",
        defaultText: undefined,
        usages: [
          {
            resource: "test.js",
            loc: { line: 1, column: 1 },
          },
        ],
      });
    });

    it("extracts the translation when $translate is a member of this", function () {
      let translateCall = b.callExpression(
        b.memberExpression(b.thisExpression(), $translate),
        [b.literal("test")]
      );

      translateCall.loc = { start: { line: 1, column: 1 } };

      visitor.visit(translateCall);

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith({
        id: "test",
        defaultText: undefined,
        usages: [
          {
            resource: "test.js",
            loc: { line: 1, column: 1 },
          },
        ],
      });
    });

    it("extracts the translation when $translate is a member", function () {
      let translateCall = b.callExpression(
        b.memberExpression(b.identifier("_this"), $translate),
        [b.literal("test")]
      );

      translateCall.loc = { start: { line: 1, column: 1 } };

      visitor.visit(translateCall);

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith({
        id: "test",
        defaultText: undefined,
        usages: [
          {
            resource: "test.js",
            loc: { line: 1, column: 1 },
          },
        ],
      });
    });

    it("emits an error if the function is called without any arguments", function () {
      let translateCall = b.callExpression($translate, []);
      translateCall.loc = { start: { line: 1, column: 1 } };

      visitor.visit(translateCall);

      expect(loaderContext).toHaveEmittedErrorMatchingSnapshot();
    });

    it("emits an error if the translation id is not an array expression and neither a literal", function () {
      let translateCall = b.callExpression($translate, [b.identifier("test")]);
      translateCall.loc = { start: { line: 1, column: 1 } };

      visitor.visit(translateCall);

      expect(loaderContext).toHaveEmittedErrorMatchingSnapshot();
    });

    it("emits an error if any translation id in the passed in array is not a literal", function () {
      let translateCall = b.callExpression($translate, [
        b.arrayExpression([b.literal("test"), b.identifier("notValid")]),
      ]);
      translateCall.loc = { start: { line: 1, column: 0 } };

      visitor.visit(translateCall);

      expect(loaderContext).toHaveEmittedErrorMatchingSnapshot();
    });

    it("emits an error if the default text is not a literal", function () {
      let translateCall = b.callExpression($translate, [
        b.literal("test"),
        b.literal(null),
        b.literal(null),
        b.identifier("test"),
      ]);
      translateCall.loc = { start: { line: 1, column: 1 } };

      visitor.visit(translateCall);

      expect(loaderContext).toHaveEmittedErrorMatchingSnapshot();
    });

    it("suppress the call needs at least one argument error if block contains 'suppress-dynamic-translation-error: true' comment", function () {
      let lineComment = b.line("suppress-dynamic-translation-error: true");
      lineComment.loc = {
        start: { line: 1, column: 1 },
        end: { line: 1, column: 38 },
      };
      visitor.comments.push(lineComment);

      let translateCall = b.callExpression($translate, []);
      translateCall.loc = { start: { line: 2, column: 1 } };

      let root = b.program([b.expressionStatement(translateCall)]);
      root.loc = {
        start: { line: 1, column: 1 },
        end: { line: 2, column: 12 },
      };

      visitor.visit(root);

      expect(loaderContext.emitError).not.toHaveBeenCalled();
      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
    });

    it("suppress the id needs to be a literal error if block contains 'suppress-dynamic-translation-error: true' comment", function () {
      let lineComment = b.line("suppress-dynamic-translation-error: true");
      lineComment.loc = {
        start: { line: 1, column: 1 },
        end: { line: 1, column: 38 },
      };
      visitor.comments.push(lineComment);

      let translateCall = b.callExpression($translate, [b.identifier("test")]);
      translateCall.loc = { start: { line: 2, column: 1 } };

      let root = b.program([b.expressionStatement(translateCall)]);
      root.loc = {
        start: { line: 1, column: 1 },
        end: { line: 2, column: 12 },
      };

      visitor.visit(root);

      expect(loaderContext.emitError).not.toHaveBeenCalled();
      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
    });

    it("suppress the default value needs to be a literal error if block contains 'suppress-dynamic-translation-error: true' comment", function () {
      let lineComment = b.line("suppress-dynamic-translation-error: true");
      lineComment.loc = {
        start: { line: 1, column: 1 },
        end: { line: 1, column: 38 },
      };
      visitor.comments.push(lineComment);

      let translateCall = b.callExpression($translate, [
        b.literal("test"),
        b.literal(null),
        b.literal(null),
        b.identifier("defaultText"),
      ]);
      translateCall.loc = { start: { line: 2, column: 1 } };

      let root = b.program([b.expressionStatement(translateCall)]);
      root.loc = {
        start: { line: 1, column: 1 },
        end: { line: 2, column: 12 },
      };

      visitor.visit(root);

      expect(loaderContext.emitError).not.toHaveBeenCalled();
      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
    });
  });

  describe("i18n.registerTranslation", function () {
    "use strict";

    let i18n;
    let registerTranslation;

    beforeEach(function () {
      i18n = b.identifier("i18n");
      registerTranslation = b.memberExpression(
        i18n,
        b.identifier("registerTranslation")
      );
    });

    it("extracts the translation with it's id from a i18n.registerTranslation call with a single argument", function () {
      let registerTranslationCall = b.callExpression(registerTranslation, [
        b.literal("test"),
      ]);
      registerTranslationCall.loc = { start: { line: 1, column: 1 } };

      let ast = visitor.visit(registerTranslationCall);

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith({
        id: "test",
        defaultText: undefined,
        usages: [
          {
            resource: "test.js",
            loc: { line: 1, column: 1 },
          },
        ],
      });

      expect(visitor.changedAst).toBe(true);
      expect(n.Literal.check(ast)).toBe(true);
      expect(ast.value).toBe("test");
    });

    it("extracts the translation with it's id and default text from a i18n.registerTranslation call with a two arguments", function () {
      let registerTranslationCall = b.callExpression(registerTranslation, [
        b.literal("test"),
        b.literal("default Text"),
      ]);
      registerTranslationCall.loc = { start: { line: 1, column: 1 } };

      let ast = visitor.visit(registerTranslationCall);

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith({
        id: "test",
        defaultText: "default Text",
        usages: [
          {
            resource: "test.js",
            loc: { line: 1, column: 1 },
          },
        ],
      });

      expect(visitor.changedAst).toBe(true);
      expect(n.Literal.check(ast)).toBe(true);
      expect(ast.value).toBe("test");
    });

    it("emits an error if called without arguments", function () {
      let registerTranslationCall = b.callExpression(registerTranslation, []);
      registerTranslationCall.loc = { start: { line: 1, column: 1 } };

      visitor.visit(registerTranslationCall);

      expect(loaderContext).toHaveEmittedErrorMatchingSnapshot();
    });

    it("emits an error if the translation is not a literal", function () {
      let registerTranslationCall = b.callExpression(registerTranslation, [
        b.identifier("test"),
      ]);
      registerTranslationCall.loc = { start: { line: 1, column: 1 } };

      visitor.visit(registerTranslationCall);

      expect(loaderContext).toHaveEmittedErrorMatchingSnapshot();
    });

    it("emits an error if the default text is not a literal", function () {
      let registerTranslationCall = b.callExpression(registerTranslation, [
        b.literal("test"),
        b.identifier("defaultText"),
      ]);
      registerTranslationCall.loc = { start: { line: 1, column: 1 } };

      visitor.visit(registerTranslationCall);

      expect(loaderContext).toHaveEmittedErrorMatchingSnapshot();
    });
  });

  describe("i18n.registerTranslations", function () {
    "use strict";

    let i18n;
    let registerTranslations;

    beforeEach(function () {
      i18n = b.identifier("i18n");
      registerTranslations = b.memberExpression(
        i18n,
        b.identifier("registerTranslations")
      );
    });

    it("can process empty registerTranslations calls", function () {
      let registerTranslationCall = b.callExpression(registerTranslations, [
        b.objectExpression([]),
      ]);

      let ast = visitor.visit(registerTranslationCall);

      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
      expect(visitor.changedAst).toBe(true);
      expect(n.ArrayExpression.check(ast)).toBe(true);
    });

    it("extracts the translation with it's id and default text from a i18n.registerTranslations call", function () {
      let registerTranslationsCall = b.callExpression(registerTranslations, [
        b.objectExpression([
          b.property("init", b.identifier("test"), b.literal("Test")),
          b.property("init", b.identifier("x"), b.literal("X")),
        ]),
      ]);
      registerTranslationsCall.loc = { start: { line: 1, column: 1 } };

      let ast = visitor.visit(registerTranslationsCall);

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith({
        id: "test",
        defaultText: "Test",
        usages: [
          {
            resource: "test.js",
            loc: { line: 1, column: 1 },
          },
        ],
      });

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith({
        id: "x",
        defaultText: "X",
        usages: [
          {
            resource: "test.js",
            loc: { line: 1, column: 1 },
          },
        ],
      });

      expect(visitor.changedAst).toBe(true);
      expect(n.ArrayExpression.check(ast)).toBe(true);
      expect(ast.elements).toHaveLength(2);
      expect(n.Literal.check(ast.elements[0])).toBe(true);
      expect(n.Literal.check(ast.elements[1])).toBe(true);
      expect(ast.elements[0].value).toBe("test");
      expect(ast.elements[1].value).toBe("x");
    });

    // TODO bad cases
  });

  describe("isCommentedWithSuppressError", function () {
    "use strict";
    let comments;

    beforeEach(function () {
      comments = [];
    });

    /**
     * $translate();
     */
    it("returns false for a program without a comment", function () {
      let program = b.program([
        b.expressionStatement(b.callExpression(b.identifier("$translate"), [])),
      ]);

      program.loc = {
        start: { line: 1, column: 1 },
        end: { line: 1, column: 12 },
      };
      let root = new NodePath({ root: program }).get("root");

      expect(isCommentedWithSuppressError(root, comments)).toBe(false);
    });

    /**
     * // suppress-dynamic-translation-error: true
     * $translate();
     */
    it("returns true for a program with a comment", function () {
      let lineComment = b.line("suppress-dynamic-translation-error: true");
      lineComment.loc = {
        start: { line: 1, column: 1 },
        end: { line: 1, column: 38 },
      };
      comments.push(lineComment);

      let program = b.program([
        b.expressionStatement(b.callExpression(b.identifier("$translate"), [])),
      ]);
      program.loc = {
        start: { line: 1, column: 1 },
        end: { line: 2, column: 12 },
      };

      let root = new NodePath({ root: program }).get("root");

      expect(isCommentedWithSuppressError(root, comments)).toBe(true);
    });

    /**
     * // suppress-dynamic-translation-error: true
     * $translate();
     */
    it("returns true if the containing program contains a comment", function () {
      let lineComment = b.line("suppress-dynamic-translation-error: true");
      lineComment.loc = {
        start: { line: 1, column: 1 },
        end: { line: 1, column: 38 },
      };
      comments.push(lineComment);

      let expression = b.expressionStatement(
        b.callExpression(b.identifier("$translate"), [])
      );
      expression.loc = {
        start: { line: 2, column: 1 },
        end: { line: 2, column: 12 },
      };

      let program = b.program([expression]);
      program.loc = {
        start: { line: 1, column: 1 },
        end: { line: 2, column: 12 },
      };

      let root = new NodePath({ root: program }).get("root");
      expect(
        isCommentedWithSuppressError(root.get("body").get(0), comments)
      ).toBe(true);
    });

    /**
     * {
     *      // suppress-dynamic-translation-error: true
     *      $translate();
     *  }
     */
    it("returns true if a parent block contains a comment", function () {
      let lineComment = b.line("suppress-dynamic-translation-error: true");
      lineComment.loc = {
        start: { line: 2, column: 1 },
        end: { line: 2, column: 38 },
      };
      comments.push(lineComment);

      let expression = b.expressionStatement(
        b.callExpression(b.identifier("$translate"), [])
      );
      expression.loc = {
        start: { line: 3, column: 1 },
        end: { line: 3, column: 12 },
      };

      let block = b.blockStatement([expression]);
      block.loc = {
        start: { line: 1, column: 1 },
        end: { line: 4, column: 1 },
      };

      let program = b.program([block]);
      program.loc = {
        start: { line: 1, column: 1 },
        end: { line: 4, column: 1 },
      };

      let root = new NodePath({ root: program }).get("root");

      expect(
        isCommentedWithSuppressError(
          root.get("body").get(0).get("body").get(0),
          comments
        )
      ).toBe(true);
    });

    /**
     * $translate();
     * {
     *      // suppress-dynamic-translation-error: true
     * }
     */
    it("returns false if a sibling block contains a comment", function () {
      let expression = b.expressionStatement(
        b.callExpression(b.identifier("$translate"), [])
      );
      expression.loc = {
        start: { line: 1, column: 1 },
        end: { line: 1, column: 12 },
      };

      let block = b.blockStatement([expression]);
      block.loc = {
        start: { line: 2, column: 1 },
        end: { line: 4, column: 1 },
      };

      let lineComment = b.line("suppress-dynamic-translation-error: true");
      lineComment.loc = {
        start: { line: 3, column: 1 },
        end: { line: 3, column: 38 },
      };
      comments.push(lineComment);

      let program = b.program([expression, block]);
      program.loc = {
        start: { line: 1, column: 1 },
        end: { line: 4, column: 1 },
      };

      let root = new NodePath({ root: program }).get("root");

      expect(
        isCommentedWithSuppressError(root.get("body").get(0), comments)
      ).toBe(false);
    });
  });
});


<|start_filename|>test/html/angular-i18n-translations-extractor.spec.js<|end_filename|>
import angularI18nTranslationsExtractor from "../../src/html/angular-i18n-translations-extractor";
import StatefulHtmlParser from "../../src/html/translate-html-parser";
import Translation from "../../src/translation";

require("../translate-jest-matchers");

describe("StatefulHtmlParserSpecs", function() {
  "use strict";

  let loaderContext;

  beforeEach(function() {
    loaderContext = {
      registerTranslation: jest.fn(),
      emitError: jest.fn(),
      emitWarning: jest.fn(),
      resourcePath: "path/test.html",
      context: "path"
    };
  });

  describe("<any i18n>", function() {
    it("uses the value of the i18n attribute without the '@@' as translation id and the content as default translation", function() {
      parse("<div i18n='@@Simple Case id'>Simple case</div>");

      expect(loaderContext.registerTranslation).toHaveBeenLastCalledWith(
        new Translation("Simple Case id", "Simple case", {
          resource: "test.html",
          loc: { line: 1, column: 0 }
        })
      );
    });

    it("emits an error if the attribute value does not contain the id indicator '@@'.", function() {
      parse("<div i18n='Simple Case id'>Simple case</div>");

      expect(loaderContext).toHaveEmittedErrorMatchingSnapshot();
      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
    });

    it("emits an error if the attribute value contains an empty id", function() {
      parse("<div i18n='@@'>Simple case</div>");

      expect(loaderContext).toHaveEmittedErrorMatchingSnapshot();
      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
    });

    it("emits an error if no default translation is provided", function() {
      parse("<div i18n='@@Simple Case id'></div>");

      expect(loaderContext).toHaveEmittedErrorMatchingSnapshot();
      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
    });

    it("emits an error if the element contains multiple child nodes", function() {
      parse(
        "<div i18n='@@Simple Case id'>Created by <b><NAME></b> at 13:34</div>"
      );

      expect(loaderContext).toHaveEmittedErrorMatchingSnapshot();
      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
    });

    it("emits an error if the content of the element is an expression", function() {
      parse("<div i18n='@@Simple Case id'>{{someValue}}</div>");

      expect(loaderContext).toHaveEmittedErrorMatchingSnapshot();
      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
    });

    it("emits an error if the value of the translate id is an expression", function() {
      parse("<div i18n='@@{{id}}'>Not an expression</div>");

      expect(loaderContext).toHaveEmittedErrorMatchingSnapshot();
      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
    });

    it("suppresses the error if the translation element is an expression and the element is attributed with suppress-dynamic-translation-error", function() {
      parse(
        "<div suppress-dynamic-translation-error> i18n='@@Simple Case id'>{{someValue}}</div>"
      );

      expect(loaderContext.emitError).not.toHaveBeenCalled();
      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
    });

    it("suppresses the error if the default text is an expression and the element is attributed with suppress-dynamic-translation-error", function() {
      parse(
        "<div i18n='@@{{id}}' suppress-dynamic-translation-error>Not an expression</div>"
      );

      expect(loaderContext.emitError).not.toHaveBeenCalled();
      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
    });
  });

  describe("<any i18n-*>", function() {
    it("uses the value of an i18n-[attr] without '@@' as translation id and the value of the [attr] as default text", function() {
      parse(
        "<div i18n-title='@@test attribute id' title='test attribute'></div>"
      );

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith(
        new Translation("test attribute id", "test attribute", {
          resource: "test.html",
          loc: { line: 1, column: 0 }
        })
      );
    });

    it("emits an error if the attribute value does not contain the id indicator '@@'.", function() {
      parse(
        "<div i18n-title='test attribute id' title='test attribute'></div>"
      );

      expect(loaderContext).toHaveEmittedErrorMatchingSnapshot();
      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
    });

    it("emits an error if the attribute value contains an empty id", function() {
      parse("<div i18n-title='@@' title='test attribute'></div>");

      expect(loaderContext).toHaveEmittedErrorMatchingSnapshot();
      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
    });

    it("emits an error if no corresponding attribute [attr] exist for the i18n-[attr] attribute", function() {
      parse("<div i18n-title='@@test attribute id'></div>");

      expect(loaderContext).toHaveEmittedErrorMatchingSnapshot();
      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
    });

    it("emits an error if the corresponding attribute [attr] is empty", function() {
      parse("<div i18n-title='@@test attribute id' title=''></div>");

      expect(loaderContext).toHaveEmittedErrorMatchingSnapshot();
      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
    });

    it("emits an error if the default translation is an expression", function() {
      parse("<div  i18n-title='@@myTitle' title='{{myTitle}}'></div>");

      expect(loaderContext).toHaveEmittedErrorMatchingSnapshot();
      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
    });

    it("emits an error if the i18n-[attr] uses an expression as id", function() {
      parse("<div i18n-title='@@{{id}}' title='My title'></div>");

      expect(loaderContext).toHaveEmittedErrorMatchingSnapshot();
      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
    });

    it("suppresses the error if the i18n-[attr] id is used and the element is attributed with suppress-dynamic-translation-error", function() {
      parse(
        "<div i18n-title='@@{{id}}' title='My title' suppress-dynamic-translation-error></div>"
      );

      expect(loaderContext.emitError).not.toHaveBeenCalled();
      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
    });

    it("suppresses the error if the default text is an expression and the element is attributed with suppress-dynamic-translation-error", function() {
      parse(
        "<div  i18n-title='@@myTitle' title='{{myTitle}}' suppress-dynamic-translation-error></div>"
      );

      expect(loaderContext.emitError).not.toHaveBeenCalled();
      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
    });
  });

  function parse(source) {
    let statefulParser = new StatefulHtmlParser(loaderContext, [
      angularI18nTranslationsExtractor
    ]);
    statefulParser.parse(source);
    return statefulParser;
  }
});


<|start_filename|>test/html/element-context.spec.js<|end_filename|>
// TODO for some reasons the tests start to fail if importedf rom src
import { DocumentContext } from "../../src/html/element-context";

describe("ElementContext", function() {
  "use strict";

  describe("enter", function() {
    let documentContext;

    beforeEach(function() {
      documentContext = new DocumentContext(
        "test.html",
        `<body class="test"></body>`
      );
    });

    it("sets the parent element correctly", function() {
      var child = documentContext.enter("body", [
        { name: "class", value: "test" }
      ]);

      expect(child.parent).toBe(documentContext);
    });

    it("sets the element name and attributes correctly", function() {
      var child = documentContext.enter("body", [
        { name: "class", value: "test" }
      ]);

      expect(child.tagName).toBe("body");
      expect(child.attributes).toEqual([{ name: "class", value: "test" }]);
    });
  });

  describe("leave", function() {
    it("returns the previous / parent element", function() {
      const documentContext = new DocumentContext(
        "test.html",
        `<body class="test"></body>`
      );
      var child = documentContext.enter("body", [
        { name: "class", value: "test" }
      ]);

      expect(child.leave()).toBe(documentContext);
    });
  });

  describe("suppressDynamicTranslationErrors", function() {
    let documentContext;

    beforeEach(function() {
      documentContext = new DocumentContext(
        "test.html",
        `<body class="test"></body>`
      );
    });

    it("is false by default", function() {
      expect(documentContext.suppressDynamicTranslationErrors).toBe(false);
    });

    it("is true if activated on the current element", function() {
      documentContext.suppressDynamicTranslationErrors = true;
      expect(documentContext.suppressDynamicTranslationErrors).toBe(true);
    });

    it("is true if activated on a parent element", function() {
      documentContext.suppressDynamicTranslationErrors = true;
      var child = documentContext.enter("body");

      expect(child.suppressDynamicTranslationErrors).toBe(true);
    });

    it("is false if activated on a child element", function() {
      var child = documentContext.enter("body");
      child.suppressDynamicTranslationErrors = true;

      expect(documentContext.suppressDynamicTranslationErrors).toBe(false);
    });
  });

  describe("asHtml", function() {
    it("shows the html for the element", function() {
      var body = new DocumentContext("test.html", "<body></body>").enter(
        "body"
      );

      expect(body.asHtml()).toBe("<body>...</body>");
    });

    it("displays the text content of the element", function() {
      var body = new DocumentContext(
        "test.html",
        "<body>Hello World\n</body>"
      ).enter("body");

      body.addText({
        raw: "Hello World\n",
        text: "Hello World"
      });

      expect(body.asHtml()).toBe("<body>Hello World\n</body>");
    });

    it("adds the attributes to the element", function() {
      var body = new DocumentContext(
        "test.html",
        `<body class="test" id="main"></body>`
      ).enter("body", [
        {
          name: "class",
          expressions: [],
          value: "test"
        },
        {
          name: "id",
          value: "main",
          expressions: []
        }
      ]);

      expect(body.asHtml()).toBe("<body class='test' id='main'>...</body>");
    });
  });
});


<|start_filename|>test/html/ng-filters.spec.js<|end_filename|>
import { matchAngularExpressions } from "../../src/html/ng-filters";

describe("the filter expression matches angular-filters anywhere in the code using {{}}", function() {
  "use strict";

  it("matches a simple translate filter", function() {
    let result = matchAngularExpressions("{{ 'Hy' | translate }}");

    expect(result).toEqual([
      {
        value: "'Hy'",
        match: "{{ 'Hy' | translate }}",
        previousFilters: undefined
      }
    ]);
  });

  it("matches an expression with another filter before translate", function() {
    let result = matchAngularExpressions("{{ 'Hy' | currency | translate }}");

    expect(result).toEqual([
      {
        value: "'Hy'",
        match: "{{ 'Hy' | currency | translate }}",
        previousFilters: "currency"
      }
    ]);
  });

  it("matches an expression with a filter following translate", function() {
    let result = matchAngularExpressions("{{ 'Hy' | translate | limitTo:6 }}");

    expect(result).toEqual([
      {
        value: "'Hy'",
        match: "{{ 'Hy' | translate | limitTo:6 }}",
        previousFilters: undefined
      }
    ]);
  });

  it("matches an expression with a filter before and after translate", function() {
    let result = matchAngularExpressions(
      "{{ 'Hy' | currency | translate | limitTo:6 }}"
    );

    expect(result).toEqual([
      {
        value: "'Hy'",
        match: "{{ 'Hy' | currency | translate | limitTo:6 }}",
        previousFilters: "currency"
      }
    ]);
  });

  it("matches multiple expressions", function() {
    let result = matchAngularExpressions(
      "{{ 'Hy' | translate }}<span title=\"{{ 'Login' | translate}}\"></span>"
    );

    expect(result).toEqual([
      {
        value: "'Hy'",
        match: "{{ 'Hy' | translate }}",
        previousFilters: undefined
      },
      {
        value: "'Login'",
        match: "{{ 'Login' | translate}}",
        previousFilters: undefined
      }
    ]);
  });

  it("matches an attribute with translate followed by another filter without spaces", function() {
    let result = matchAngularExpressions('{{"name"|translate|limitTo:5}}');

    expect(result).toEqual([
      {
        value: '"name"',
        match: '{{"name"|translate|limitTo:5}}',
        previousFilters: undefined
      }
    ]);
  });

  it("matches an attribute with translate where translate follows another filter without spaces", function() {
    let result = matchAngularExpressions('{{"name"|currency|translate}}');

    expect(result).toEqual([
      {
        value: '"name"',
        match: '{{"name"|currency|translate}}',
        previousFilters: "currency"
      }
    ]);
  });

  it("matches property expressions", function() {
    let result = matchAngularExpressions("{{user.sex | translate}}");

    expect(result).toEqual([
      {
        value: "user.sex",
        match: "{{user.sex | translate}}",
        previousFilters: undefined
      }
    ]);
  });

  it("matches the expression inside a string", function() {
    let result = matchAngularExpressions(
      'Static Text: {{ "CHF" | translate }}'
    );

    expect(result).toEqual([
      {
        value: '"CHF"',
        match: '{{ "CHF" | translate }}',
        previousFilters: undefined
      }
    ]);
  });

  it("doesn't match an expression without translate filter", function() {
    let result = matchAngularExpressions("{{ 1 + 2 }}");

    expect(result).toEqual([]);
  });

  it("doesn't match single curly braces ", function() {
    let result = matchAngularExpressions("{{ '{{1 + 2}}' }}");

    expect(result).toEqual([]);
  });

  it("doesn't match '| translate", function() {
    let result = matchAngularExpressions("{{| translate}}");

    expect(result).toEqual([]);
  });

  it("doesn't match a string literal that looks like an expression but misses the {}", function() {
    let result = matchAngularExpressions('"value" | translate');

    expect(result).toEqual([]);
  });

  /**
   * Not supported in the current version
   */
  it("doesn't match the pipe in the string value", function() {
    let result = matchAngularExpressions('{{"value|test" | translate}}');

    expect(result).toEqual([
      {
        value: '"value|test"',
        match: '{{"value|test" | translate}}',
        previousFilters: undefined
      }
    ]);
  });
});


<|start_filename|>test/html/translate-html-parser.spec.js<|end_filename|>
import translateDirectiveTranslationExtractor from "../../src/html/translate-directive-translation-extractor";
import StatefulHtmlParser from "../../src/html/translate-html-parser";
import Translation from "../../src/translation";

require("../translate-jest-matchers");

describe("StatefulHtmlParserSpecs", function() {
  "use strict";

  let loaderContext;

  beforeEach(function() {
    loaderContext = {
      registerTranslation: jest.fn(),
      emitError: jest.fn(),
      emitWarning: jest.fn(),
      resourcePath: "path/test.html",
      context: "path"
    };
  });

  describe("<translate>", function() {
    it("uses the element text as translation id", function() {
      parse("<translate>Simple</translate>");

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith(
        new Translation("Simple", undefined, {
          resource: "test.html",
          loc: { line: 1, column: 0 }
        })
      );
    });

    it("uses the translate attribute value as id", function() {
      parse("<translate translate='simple-id'>Simple</translate>");

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith(
        new Translation("simple-id", undefined, {
          resource: "test.html",
          loc: { line: 1, column: 0 }
        })
      );
    });

    it("uses the value of the translate-default as defaultText", function() {
      parse("<translate translate-default='Other default'>Simple</translate>");

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith(
        new Translation("Simple", "Other default", {
          resource: "test.html",
          loc: { line: 1, column: 0 }
        })
      );
    });

    it("only translates the attribute if the translation id is undefined", function() {
      parse("<translate translate-attr-title='Simple'></translate>");

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith(
        new Translation("Simple", undefined, {
          resource: "test.html",
          loc: { line: 1, column: 0 }
        })
      );

      expect(loaderContext.registerTranslation).toHaveBeenCalledTimes(1);
    });

    it("only translates the attribute if the translation id is empty", function() {
      parse("<translate translate-attr-title='Simple'>    </translate>");

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith(
        new Translation("Simple", undefined, {
          resource: "test.html",
          loc: { line: 1, column: 0 }
        })
      );

      expect(loaderContext.registerTranslation).toHaveBeenCalledTimes(1);
    });

    it("translates the attribute and the content of the element if translate-attr is set and the element has non empty content", function() {
      parse(
        "<translate translate-attr-title='Attribute'>Element-Text</translate>"
      );

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith(
        new Translation("Attribute", undefined, {
          resource: "test.html",
          loc: { line: 1, column: 0 }
        })
      );

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith(
        new Translation("Element-Text", undefined, {
          resource: "test.html",
          loc: { line: 1, column: 0 }
        })
      );
    });

    it("emits an error if the content of the element is an expression", function() {
      parse("<translate>{{controller.title}}</translate>");

      expect(loaderContext).toHaveEmittedErrorMatchingSnapshot();
    });

    it("emits an error if the default text is an expression", function() {
      parse(
        "<translate translate-default='{{controller.title}}'>Simple</translate>"
      );

      expect(loaderContext).toHaveEmittedErrorMatchingSnapshot();
    });

    it("suppresses the error if the translation element is an expression and the element is attributed with suppress-dynamic-translation-error", function() {
      parse(
        "<translate suppress-dynamic-translation-error>{{controller.title}}</translate>"
      );

      expect(loaderContext.emitError).not.toHaveBeenCalled();
      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
    });

    it("suppresses the error if the default text is an expression and the element is attributed with suppress-dynamic-translation-error", function() {
      parse(
        "<translate translate-default='{{controller.title}}' suppress-dynamic-translation-error>simple</translate>"
      );

      expect(loaderContext.emitError).not.toHaveBeenCalled();
      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
    });
  });

  describe("<any translate>", function() {
    it("uses the value of the translate-attribute as translation id", function() {
      parse("<div translate='Simple'></div>");

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith(
        new Translation("Simple", undefined, {
          resource: "test.html",
          loc: { line: 1, column: 0 }
        })
      );
    });

    it("uses the content as translation id if the translate-attribute has no value assigned", function() {
      parse("<div translate>Simple</div>");

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith(
        new Translation("Simple", undefined, {
          resource: "test.html",
          loc: { line: 1, column: 0 }
        })
      );
    });

    it("uses the value of the default-text attribute as default text.", function() {
      parse("<div translate translate-default='Other default'>Simple</div>");

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith(
        new Translation("Simple", "Other default", {
          resource: "test.html",
          loc: { line: 1, column: 0 }
        })
      );
    });

    it("only translates the attribute if the element content is empty", function() {
      parse("<div translate translate-attr-title='Simple'></div>");

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith(
        new Translation("Simple", undefined, {
          resource: "test.html",
          loc: { line: 1, column: 0 }
        })
      );

      expect(loaderContext.registerTranslation).toHaveBeenCalledTimes(1);
    });

    it("only translates the attribute if the translation id is empty", function() {
      parse("<div translate translate-attr-title='Simple'></div>");

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith(
        new Translation("Simple", undefined, {
          resource: "test.html",
          loc: { line: 1, column: 0 }
        })
      );

      expect(loaderContext.registerTranslation).toHaveBeenCalledTimes(1);
    });

    it("translates the attribute and the content of the element if translate-attr is set and the element has non empty content", function() {
      parse(
        "<div translate translate-attr-title='Attribute'>Element-Text</div>"
      );

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith(
        new Translation("Attribute", undefined, {
          resource: "test.html",
          loc: { line: 1, column: 0 }
        })
      );

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith(
        new Translation("Element-Text", undefined, {
          resource: "test.html",
          loc: { line: 1, column: 0 }
        })
      );
    });

    it("translates the attribute and the content of the element if translate-attr is set and the translate attribute has an assigned value", function() {
      parse(
        "<div translate='Element-Text' translate-attr-title='Attribute'></div>"
      );

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith(
        new Translation("Attribute", undefined, {
          resource: "test.html",
          loc: { line: 1, column: 0 }
        })
      );

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith(
        new Translation("Element-Text", undefined, {
          resource: "test.html",
          loc: { line: 1, column: 0 }
        })
      );
    });

    it("emits an error if the value of the translate id is an expression", function() {
      parse("<div translate='{{controller.title}}'></div>");

      expect(loaderContext).toHaveEmittedErrorMatchingSnapshot();
    });

    it("suppresses the error of a dynamic value in the translation id attribute if the element is attributed with suppress-dynamic-translation-error", function() {
      parse(
        "<div translate='{{controller.title}}' suppress-dynamic-translation-error></div>"
      );
      expect(loaderContext.emitError).not.toHaveBeenCalled();
      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
    });

    it("emits an error if a translation does not have a valid id", function() {
      parse("<title translate>\n     </title>");

      expect(loaderContext).toHaveEmittedErrorMatchingSnapshot();
      // ensure the translation is not registered a second time because of a test if scope.text is falsy (what is the case above).
      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
    });

    it("doesn't emit an error if a translation does not have a valid id but an attribute has been translated", function() {
      parse("<title translate translate-attr-title='test'>\n     </title>");

      expect(loaderContext.emitError).not.toHaveBeenCalled();
    });
  });

  describe("<any translate translate-attr-*>", function() {
    it("uses the value of the translate-attr-title attribute as translation-id", function() {
      parse("<div translate translate-attr-title='test'></div>");

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith(
        new Translation("test", undefined, {
          resource: "test.html",
          loc: { line: 1, column: 0 }
        })
      );
    });

    it("uses the default text from the translate-default-attr-*", function() {
      parse(
        "<div translate translate-attr-title='test' translate-default-attr-title='Default Text'></div>"
      );

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith(
        new Translation("test", "Default Text", {
          resource: "test.html",
          loc: { line: 1, column: 0 }
        })
      );
    });

    it("doesn't register a translation for the content of an element attributed with translate-attr", function() {
      parse(
        "<div translate translate-attr-title='test'><span>Test</span></div>"
      );

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith(
        new Translation("test", undefined, {
          resource: "test.html",
          loc: { line: 1, column: 0 }
        })
      );

      expect(loaderContext.registerTranslation).toHaveBeenCalledTimes(1);
    });

    it("emits an error if translate-attr uses an expression", function() {
      parse(
        "<div translate translate-attr-title='{{controller.title}}'></div>"
      );

      expect(loaderContext).toHaveEmittedErrorMatchingSnapshot();
      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
    });

    it("suppresses the error if translate-attr is used and the element is attributed with suppress-dynamic-translation-error", function() {
      parse(
        "<div translate translate-attr-title='{{controller.title}}' suppress-dynamic-translation-error></div>"
      );

      expect(loaderContext.emitError).not.toHaveBeenCalled();
      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
    });
  });

  describe("{{ any | translate }}", function() {
    it("extracts the translation id of a translate filter with a literal value", function() {
      parse("<root>{{ 'test' | translate }}</root>");

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith(
        new Translation("test", undefined, {
          resource: "test.html",
          loc: { line: 1, column: 6 }
        })
      );
    });

    it("extracts the translation id if the translate filter is the first in chain and a literal value is used", function() {
      parse("<root>{{ 'test' | translate | uppercase }}</root>");

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith(
        new Translation("test", undefined, {
          resource: "test.html",
          loc: { line: 1, column: 6 }
        })
      );
    });

    it("extracts the translation id if the translate filter is used inside a text body", function() {
      parse(
        "<root>{{ ctrl.total | number:0 }} {{ 'USD' | translate }} ($)</root>"
      );

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith(
        new Translation("USD", undefined, {
          resource: "test.html",
          loc: { line: 1, column: 6 }
        })
      );
    });

    it("emits an error if the translate filter is being used for a dynamic value", function() {
      parse("<root>{{ controller.title | translate }}</root>");

      expect(loaderContext).toHaveEmittedErrorMatchingSnapshot();
    });

    it("emits an error if the translate filter is not the first in the filter chain", function() {
      parse("<root>{{ 'title' | uppercase | translate }}</root>");

      expect(loaderContext).toHaveEmittedErrorMatchingSnapshot();
    });

    it("suppresses the error for a filter with a dynamic value if suppress-dynamic-translate-error is used on the parent element", function() {
      parse(
        "<span suppress-dynamic-translation-error>{{ controller.title | translate }}</span>"
      );

      expect(loaderContext.emitError).not.toHaveBeenCalled();
      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
    });

    it("suppresses the error for a filter chain where translate is not the first filter if suppress-dynamic-translate-error is used on the parent element", function() {
      parse(
        "<span suppress-dynamic-translation-error>{{ 'title' | uppercase | translate }}</span>"
      );

      expect(loaderContext.emitError).not.toHaveBeenCalled();
      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
    });
  });

  describe("<any attr='{{ any | translate }}'>", function() {
    it("extracts the translation from an attribute with translate filter", function() {
      parse("<img src='xy' title=\"{{ 'Waterfall' | translate }}\" />");

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith(
        new Translation("Waterfall", undefined, {
          resource: "test.html",
          loc: { line: 1, column: 0 }
        })
      );
    });

    it("extracts multiple translations from an attribute with translate filters", function() {
      parse(
        "<img src='xy' title=\"Fixed Text: {{ 'Waterfall' | translate }} and {{ 'Other' | translate }}\" />"
      );

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith(
        new Translation("Waterfall", undefined, {
          resource: "test.html",
          loc: { line: 1, column: 0 }
        })
      );

      expect(loaderContext.registerTranslation).toHaveBeenCalledWith(
        new Translation("Other", undefined, {
          resource: "test.html",
          loc: { line: 1, column: 0 }
        })
      );
    });

    it("emits an error if the translate filter is not the first in the filter chain", function() {
      parse("<img src='xz' alt='{{ \"title\" | uppercase | translate }}' />");

      expect(loaderContext).toHaveEmittedErrorMatchingSnapshot();
    });

    it("emits an error if the translate filter is used for a dynamic value", function() {
      parse("<img src='xz' alt='{{ ctrl.imgAlt | translate }}' />");

      expect(loaderContext).toHaveEmittedErrorMatchingSnapshot();
    });
  });

  describe("invalid cases", function() {
    it("does not register an empty element without the translate attribute", function() {
      parse("<div></div>");

      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
    });

    it("does not register a translation for an element without the translate attribute", function() {
      parse("<div>Test</div>");

      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
    });

    it("does not register a translation for a translate-attribute if the translate directive is missing on the element", function() {
      parse("<div translate-attr-title='test'>Test</div>");

      expect(loaderContext.registerTranslation).not.toHaveBeenCalled();
    });
  });

  function parse(source) {
    let statefulParser = new StatefulHtmlParser(loaderContext, [
      translateDirectiveTranslationExtractor
    ]);
    statefulParser.parse(source);
    return statefulParser;
  }
});


<|start_filename|>test/plugin.spec.js<|end_filename|>
import webpack from "webpack";
import deepExtend from "deep-extend";
import { Volume, createFsFromVolume } from "memfs";
import { ufs } from "unionfs";
import path from "path";
import fs from "fs";

// The main plugin can not be imported from the typescript source
// because webpack seems to modify the loader path
import * as WebPackAngularTranslate from "../dist/index";

import "./translate-jest-matchers";

/**
 * Helper function to implement tests that verify the result in the translations.js
 * @param fileName {string} the filename of the input file (the file to process by webpack)
 * @param doneCallback the done callback from mocha that is invoked when the test has completed
 * @param assertCallback {function({}, {})} Callback that contains the assert statements. the first argument
 * is the source of the translations file. The webpack stats (containing warnings and errors) is passed as second argument.
 */
async function compileAndGetTranslations(
  fileName,
  customTranslationExtractors
) {
  if (!customTranslationExtractors) {
    customTranslationExtractors = [];
  }
  var options = webpackOptions(
    {
      entry: ["./test/cases/" + fileName],
    },
    customTranslationExtractors
  );

  const { error, stats, volume } = await compile(options);
  expect(error).toBeFalsy();

  var translations = {};
  if (stats.compilation.assets["translations.json"]) {
    translations = JSON.parse(
      volume.toJSON(__dirname, undefined, true)["dist/translations.json"]
    );
  }

  return { translations, stats };
}

function webpackOptions(options, customTranslationExtractors) {
  "use strict";
  return deepExtend(
    {
      output: {
        path: path.join(__dirname, "dist"),
      },
      mode: "production",
      module: {
        rules: [
          {
            test: /\.html$/,
            use: [
              {
                loader: "html-loader",
                options: {
                  removeEmptyAttributes: false,
                  attrs: [],
                },
              },
              {
                loader: WebPackAngularTranslate.htmlLoader(),
                options: {
                  translationExtractors: customTranslationExtractors,
                },
              },
            ],
          },
          {
            test: /\.js/,
            loader: WebPackAngularTranslate.jsLoader(),
          },
        ],
      },

      plugins: [new WebPackAngularTranslate.Plugin()],
    },
    options
  );
}

function compile(options) {
  var compiler = webpack(options);
  var volume = new Volume();
  compiler.outputFileSystem = new VolumeOutputFileSystem(volume);

  return new Promise((resolve, reject) => {
    compiler.run(function (error, stats) {
      resolve({ error, stats, volume });
    });
  });
}

describe("HTML Loader", function () {
  "use strict";

  it("emits a useful error message if the plugin is missing", async function () {
    const { error, stats } = await compile({
      entry: "./test/cases/simple.html",
      output: {
        path: path.join(__dirname, "dist"),
      },
      module: {
        rules: [
          {
            test: /\.html$/,
            use: [
              {
                loader: "html-loader",
                options: {
                  removeEmptyAttributes: false,
                  attrs: [],
                },
              },
              {
                loader: WebPackAngularTranslate.htmlLoader(),
              },
            ],
          },
          {
            test: /\.js/,
            loader: WebPackAngularTranslate.jsLoader(),
          },
        ],
      },
    });

    expect(error).toBeNull();
    expect(stats.compilation.errors).toMatchInlineSnapshot(`
Array [
  ModuleBuildError: "The WebpackAngularTranslate plugin is missing. Add the plugin to your webpack configurations 'plugins' section.",
]
`);
  });

  describe("directive", function () {
    "use strict";

    it("extracts the translation id if translate is used as attribute", async function () {
      const { translations } = await compileAndGetTranslations("simple.html");

      expect(translations).toMatchObject({
        "attribute-translation": "attribute-translation",
      });
    });

    it("extracts the translation id if translate is used as element", async function () {
      const { translations } = await compileAndGetTranslations("simple.html");
      expect(translations).toMatchObject({
        "element-translation": "element-translation",
      });
    });

    it("extracts the translation id from the attribute if specified", async function () {
      const { translations } = await compileAndGetTranslations("simple.html");
      expect(translations).toMatchObject({
        "id-in-attribute": "id-in-attribute",
      });
    });

    it("extracts the default text if translate is used as attribute", async function () {
      const { translations } = await compileAndGetTranslations(
        "defaultText.html"
      );
      expect(translations).toMatchObject({ Login: "Anmelden" });
    });

    it("extracts the default text if translate is used as element", async function () {
      const { translations } = await compileAndGetTranslations(
        "defaultText.html"
      );

      expect(translations).toMatchObject({ Logout: "Abmelden" });
    });

    it("extracts the translation id if a translation for an attribute is defined", async function () {
      const { translations } = await compileAndGetTranslations(
        "attributes.html"
      );
      expect(translations).toMatchObject({
        "attribute-id": "attribute-id",
      });
    });

    it("extracts the default text for an attribute translation", async function () {
      const { translations } = await compileAndGetTranslations(
        "attributes.html"
      );
      expect(translations).toMatchObject({
        "attribute-default-id": "Default text for attribute title",
      });
    });

    it("emits an error if an angular expression is used as attribute id", async function () {
      const { stats } = await compileAndGetTranslations("expressions.html");

      expect(stats.compilation.errors).toMatchInlineSnapshot(`
Array [
  ModuleError: "Failed to extract the angular-translate translations from 'expressions.html':8:1: The element '<h1 translate=''>{{editCtrl.title}}</h1>' uses an angular expression as translation id ('{{editCtrl.title}}') or as default text ('undefined'). This is not supported. Either use a string literal as translation id and default text or suppress this error by adding the 'suppress-dynamic-translation-error' attribute to this element or any of its parents.",
]
`);
    });

    it("emits an error if a translated angular element has multiple child text elements and does not specify an id", async () => {
      const { stats } = await compileAndGetTranslations(
        "multiple-child-texts.html"
      );

      expect(stats.compilation.errors).toMatchInlineSnapshot(`
Array [
  ModuleError: "Failed to extract the angular-translate translations from 'multiple-child-texts.html':8:4: The element does not specify a translation id but has multiple child text elements. Specify the translation id on the element to define the translation id.",
]
`);
    });

    it("does suppress errors for dynamic translations if the element is attributed with suppress-dynamic-translation-error", async function () {
      const { translations, stats } = await compileAndGetTranslations(
        "expressions-suppressed.html"
      );
      expect(stats.compilation.errors).toMatchInlineSnapshot(`Array []`);
      expect(translations).toEqual({});
    });

    it("removes the suppress-dynamic-translation-error attribute for non dev build", async function () {
      const { stats } = await compileAndGetTranslations(
        "expressions-suppressed.html"
      );
      var output = stats.compilation.assets["main.js"].source();

      expect(output).toMatch("{{editCtrl.title}}");
      expect(output).not.toMatch("suppress-dynamic-translation-error");
    });
  });

  describe("filter", function () {
    it("matches a filter in the body of an element", async function () {
      const { translations } = await compileAndGetTranslations(
        "filter-simple.html"
      );
      expect(translations).toMatchObject({ Home: "Home" });
    });

    it("matches a filter in an attribute of an element", async function () {
      const { translations } = await compileAndGetTranslations(
        "filter-simple.html"
      );
      expect(translations).toMatchObject({ Waterfall: "Waterfall" });
    });

    it("matches an expression in the middle of the element text content", async function () {
      const { translations } = await compileAndGetTranslations(
        "filter-simple.html"
      );
      expect(translations).toMatchObject({ Top: "Top" });
    });

    it("matches multiple expressions in a single text", async function () {
      const { translations } = await compileAndGetTranslations(
        "multiple-filters.html"
      );
      expect(translations).toMatchObject({
        Result: "Result",
        of: "of",
      });
    });

    it("emits an error if a dynamic value is used in the translate filter", async function () {
      const { translations, stats } = await compileAndGetTranslations(
        "dynamic-filter-expression.html"
      );

      expect(stats.compilation.errors).toMatchInlineSnapshot(`
Array [
  ModuleError: "Failed to extract the angular-translate translations from 'dynamic-filter-expression.html':8:14: A dynamic filter expression is used in the text or an attribute of the element '<h1 id='top'>{{ editCtrl.title | translate }}</h1>'. Add the 'suppress-dynamic-translation-error' attribute to suppress the error (ensure that you have registered the translation manually, consider using i18n.registerTranslation).",
]
`);
      expect(translations).toEqual({});
    });

    it("emits an error if a filter is used before the translate filter", async function () {
      const { translations, stats } = await compileAndGetTranslations(
        "filter-chain.html"
      );

      expect(stats.compilation.errors).toMatchInlineSnapshot(`
Array [
  ModuleError: "Failed to extract the angular-translate translations from 'filter-chain.html':8:14: Another filter is used before the translate filter in the element <h1 id='top'>{{ \\"5.0\\" | currency | translate }}</h1>. Add the 'suppress-dynamic-translation-error' to suppress the error (ensure that you have registered the translation manually, consider using i18n.registerTranslation).",
]
`);
      expect(translations).toEqual({});
    });

    it("suppress dynamic translations errors if element or parent is attribute with suppress-dynamic-translation-error", async function () {
      const { stats } = await compileAndGetTranslations(
        "dynamic-filter-expression-suppressed.html"
      );
      expect(stats.compilation.errors).toMatchInlineSnapshot(`Array []`);
    });

    it("suppress dynamic translations errors for custom elements when attributed with suppress-dynamic-translation-error", async function () {
      const { stats } = await compileAndGetTranslations(
        "dynamic-filter-custom-element.html"
      );
      expect(stats.compilation.errors).toMatchInlineSnapshot(`Array []`);
    });

    it("can parse an invalid html file", async function () {
      const { translations } = await compileAndGetTranslations(
        "invalid-html.html"
      );
      expect(translations).toMatchObject({ Result: "Result" });
    });

    it("can parse an html containing an attribute that starts with a $", async function () {
      const { translations } = await compileAndGetTranslations(
        "html-with-dollar-attribute.html"
      );
      expect(translations).toMatchObject({ Test: "Test" });
    });
  });

  it("can be used with the angular i18n translation extractor", async function () {
    "use strict";

    const {
      translations,
    } = await compileAndGetTranslations("translate-and-i18n.html", [
      WebPackAngularTranslate.angularI18nTranslationsExtractor,
    ]);

    expect(translations).toMatchObject({
      translateId: "Translate translation",
      i18nId: "I18n translation",
    });
  });
});

describe("JSLoader", function () {
  "use strict";

  it("emits a useful error message if the plugin is missing", async function () {
    const { error, stats } = await compile({
      entry: "./test/cases/simple.js",
      output: {
        path: path.join(__dirname, "dist"),
      },
      module: {
        rules: [
          {
            test: /\.js/,
            loader: WebPackAngularTranslate.jsLoader(),
          },
        ],
      },
    });

    expect(error).toBeNull();
    expect(stats.compilation.errors).toMatchInlineSnapshot(`
Array [
  ModuleBuildError: "The WebpackAngularTranslate plugin is missing. Add the plugin to your webpack configurations 'plugins' section.",
]
`);
  });

  it("passes the acorn parser options to acorn (in this case, allows modules)", async function () {
    const { error, stats } = await compile({
      entry: "./test/cases/es-module.js",
      output: {
        path: path.join(__dirname, "dist"),
      },
      module: {
        rules: [
          {
            test: /\.js/,
            loader: WebPackAngularTranslate.jsLoader(),
            options: {
              parserOptions: {
                sourceType: "module",
              },
            },
          },
        ],
      },
      plugins: [new WebPackAngularTranslate.Plugin()],
    });
    expect(error).toBeNull();
    expect(stats.compilation.errors).toMatchInlineSnapshot(`Array []`);
  });

  describe("$translate", function () {
    it("extracts the translation id when the $translate service is used as global variable ($translate)", async function () {
      const { translations } = await compileAndGetTranslations("simple.js");
      expect(translations).toMatchObject({
        "global variable": "global variable",
      });
    });

    it("extracts the translation id when the $translate service is used in the constructor", async function () {
      const { translations } = await compileAndGetTranslations("simple.js");
      expect(translations).toMatchObject({
        "translate in constructor": "translate in constructor",
      });
    });

    it("extracts the translation id when the $translate service is used in an arrow function (() => this.$translate)", async function () {
      const { translations } = await compileAndGetTranslations("simple.js");
      expect(translations).toMatchObject({
        "translate in arrow function": "translate in arrow function",
      });
    });

    it("extracts the translation id when the $translate service is used in a member function (this.$translate)", async function () {
      const { translations } = await compileAndGetTranslations("simple.js");
      expect(translations).toMatchObject({
        "this-translate": "this-translate",
      });
    });

    it("extracts multiple translation id's when an array is passed as argument", async function () {
      const { translations } = await compileAndGetTranslations("array.js");
      expect(translations).toMatchObject({
        FIRST_PAGE: "FIRST_PAGE",
        Next: "Next",
      });
    });

    it("extracts instant translation id", async function () {
      const { translations } = await compileAndGetTranslations("instant.js");
      expect(translations).toMatchObject({
        FIRST_TRANSLATION: "FIRST_TRANSLATION",
        SECOND_TRANSLATION: "SECOND_TRANSLATION",
      });
      expect(translations).not.toHaveProperty("SKIPPED_TRANSLATION");
    });

    it("extracts the default text", async function () {
      const { translations } = await compileAndGetTranslations(
        "defaultText.js"
      );
      expect(translations).toMatchObject({ Next: "Weiter" });
    });

    it("extracts the default text when an array is passed for the id's", async function () {
      const { translations } = await compileAndGetTranslations(
        "defaultText.js"
      );
      expect(translations).toMatchObject({
        FIRST_PAGE: "Missing",
        LAST_PAGE: "Missing",
      });
    });

    it("emits errors if $translate is used with invalid arguments", async function () {
      const { translations, stats } = await compileAndGetTranslations(
        "invalid$translate.js"
      );

      expect(stats.compilation.errors).toMatchInlineSnapshot(`
Array [
  ModuleError: "Illegal argument for call to $translate: A call to $translate requires at least one argument that is the translation id. If you have registered the translation manually, you can use a /* suppress-dynamic-translation-error: true */ comment in the block of the function call to suppress this error. (invalid$translate.js:1:0)",
  ModuleError: "Illegal argument for call to $translate: The translation id should either be a string literal or an array containing string literals. If you have registered the translation manually, you can use a /* suppress-dynamic-translation-error: true */ comment in the block of the function call to suppress this error. (invalid$translate.js:4:0)",
]
`);
      expect(translations).toEqual({});
    });

    it("a comment suppress the dynamic translation errors for $translate", async function () {
      const { translations, stats } = await compileAndGetTranslations(
        "translateSuppressed.js"
      );

      expect(stats.compilation.errors).toMatchInlineSnapshot(`
Array [
  ModuleError: "Illegal argument for call to $translate: The translation id should either be a string literal or an array containing string literals. If you have registered the translation manually, you can use a /* suppress-dynamic-translation-error: true */ comment in the block of the function call to suppress this error. (translateSuppressed.js:13:4)",
]
`);
      expect(translations).toEqual({});
    });
  });

  describe("i18n.registerTranslation", function () {
    it("register translation", async function () {
      const { translations, stats } = await compileAndGetTranslations(
        "registerTranslation.js"
      );
      expect(stats.compilation.errors).toMatchInlineSnapshot(`Array []`);

      expect(translations).toEqual({
        NEW_USER: "New user",
        EDIT_USER: "Edit user",
        5: "true",
      });
    });

    it("register translation with invalid arguments", async function () {
      const { translations, stats } = await compileAndGetTranslations(
        "registerInvalidTranslation.js"
      );

      expect(stats.compilation.errors).toMatchInlineSnapshot(`
Array [
  ModuleError: "Illegal argument for call to 'i18n.registerTranslation'. The call requires at least the 'translationId' argument that needs to be a literal (registerInvalidTranslation.js:2:0)",
  ModuleError: "Illegal argument for call to 'i18n.registerTranslation'. The call requires at least the 'translationId' argument that needs to be a literal (registerInvalidTranslation.js:4:0)",
]
`);
      expect(translations).toEqual({});
    });
  });

  describe("i18n.registerTranslations", function () {
    it("register translations", async function () {
      const { translations, stats } = await compileAndGetTranslations(
        "registerTranslations.js"
      );
      expect(stats.compilation.errors).toMatchInlineSnapshot(`Array []`);

      expect(translations).toEqual({
        Login: "Anmelden",
        Logout: "Abmelden",
        Next: "Weiter",
        Back: "Zurück",
      });
    });

    it("warns about invalid translation registrations", async function () {
      const { translations, stats } = await compileAndGetTranslations(
        "registerInvalidTranslations.js"
      );

      expect(stats.compilation.errors).toMatchInlineSnapshot(`
Array [
  ModuleError: "Illegal argument for call to i18n.registerTranslations: The value for the key 'key' needs to be a literal (registerInvalidTranslations.js:5:0)",
  ModuleError: "Illegal argument for call to i18n.registerTranslations: requires a single argument that is an object where the key is the translationId and the value is the default text (registerInvalidTranslations.js:1:0)",
]
`);
      expect(translations).toEqual({});
    });
  });
});

describe("Plugin", function () {
  it("emits an error if the same id with different default texts is used", async function () {
    const { translations, stats } = await compileAndGetTranslations(
      "differentDefaultTexts.js"
    );

    expect(stats.compilation.errors).toMatchInlineSnapshot(`
Array [
  [Error: Webpack-Angular-Translate: Two translations with the same id but different default text found.
	Existing: {
  "id": "Next",
  "defaultText": "Weiter",
  "usages": [
    "differentDefaultTexts.js:5:8"
  ]
}
	New: {
  "id": "Next",
  "defaultText": "Missing",
  "usages": [
    "differentDefaultTexts.js:6:8"
  ]
}
	Please define the same default text twice or specify the default text only once.],
]
`);
    expect(translations).toEqual({});
  });

  it("emits a warning if the translation id is missing", async function () {
    const { translations, stats } = await compileAndGetTranslations(
      "emptyTranslate.html"
    );
    expect(stats.compilation.warnings).toHaveLength(1);

    expect(stats.compilation.warnings).toMatchInlineSnapshot(`
Array [
  [Error: Invalid angular-translate translation found: The id of the translation is empty. Consider removing the translate attribute (html) or defining the translation id (js).
Translation:
'{
  "id": "",
  "defaultText": null,
  "usages": [
    "emptyTranslate.html:5:8"
  ]
}'],
]
`);

    expect(translations).toEqual({});
  });

  it("does not add translations twice if file is recompiled after change", async function () {
    const projectVolume = Volume.fromJSON(
      {
        "./fileChange.js":
          "require('./otherFile.js');\n" +
          "i18n.registerTranslation('NEW_USER', 'New user');\n" +
          "i18n.registerTranslation('DELETE_USER', 'Delete User');\n" +
          "i18n.registerTranslation('WillBeDeleted', 'Delete');",

        "./otherFile.js":
          "i18n.registerTranslation('DELETE_USER', 'Delete User');",
      },
      path.join(__dirname, "..")
    );
    const inputFs = ufs.use(fs).use(createFsFromVolume(projectVolume));
    const outputVolume = Volume.fromJSON({}, __dirname);

    var options = webpackOptions({
      entry: "./fileChange.js",
    });
    var compiler = webpack(options);
    compiler.inputFileSystem = inputFs;
    compiler.outputFileSystem = new VolumeOutputFileSystem(outputVolume);

    var secondCompilationStats = await new Promise((resolve, reject) => {
      var firstRun = true;
      var watching = compiler.watch({}, function (error, stats) {
        if (error) {
          return reject(error);
        }

        if (firstRun) {
          if (stats.compilation.errors.length > 0) {
            return reject(stats.compilation.errors);
          }

          firstRun = false;
          projectVolume.writeFileSync(
            "./fileChange.js",
            "i18n.registerTranslation('NEW_USER', 'Neuer Benutzer');"
          );

          watching.invalidate(); // watch doesn't seem to work with memory fs
        } else {
          watching.close(() => resolve(stats));
        }
      });
    });

    expect(secondCompilationStats.compilation.errors).toHaveLength(0);

    var translations = JSON.parse(
      outputVolume.toJSON(__dirname, undefined, true)["dist/translations.json"]
    );

    expect(translations).toEqual({
      NEW_USER: "Neuer Benutzer",
      DELETE_USER: "Delete User",
    });
  });
});

class VolumeOutputFileSystem {
  constructor(volume) {
    const fs = createFsFromVolume(volume);
    this.mkdirp = fs.mkdirp;
    this.mkdir = fs.mkdir.bind(fs);
    this.rmdir = fs.rmdir.bind(fs);
    this.unlink = fs.unlink.bind(fs);
    this.writeFile = fs.writeFile.bind(fs);
    this.join = path.join.bind(path);
  }
}
