');
+ });
+});
diff --git a/js/web/mergergame/test/mergergame.js b/js/web/mergergame/test/mergergame.js
new file mode 100644
index 000000000..664fd417c
--- /dev/null
+++ b/js/web/mergergame/test/mergergame.js
@@ -0,0 +1,1038 @@
+describe(tt.getModuleSuiteName(), function() {
+ describe('- test board', function() {
+ beforeEach(function() {
+ mergerGame.types = ['top', 'bottom', 'full'];
+ });
+
+ describe('(base)', function() {
+ // hand-crafted cases to test specific behaviours
+ it('empty', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const free = {"none":[0,0,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(0);
+ expect(result.progress).toBe(0);
+ });
+
+ it('one L1 bottom', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const free = {"none":[0,0,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(0);
+ expect(result.progress).toBe(0);
+ });
+
+ it('one L1 top', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,0,0], "top":[1,0,0,0], "full":[0,0,0,0]};
+ const free = {"none":[0,0,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(0);
+ expect(result.progress).toBe(0);
+ });
+
+ it('one L1 free', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const free = {"none":[1,0,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(0);
+ expect(result.progress).toBe(0);
+ });
+
+ it('one L1 bot merge', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const free = {"none":[1,0,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(0);
+ expect(result.progress).toBe(1);
+ });
+
+ it('one L1 top merge', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,0,0], "top":[1,0,0,0], "full":[0,0,0,0]};
+ const free = {"none":[1,0,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(0);
+ expect(result.progress).toBe(1);
+ });
+
+ it('two L1 free', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const free = {"none":[2,0,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(0);
+ expect(result.progress).toBe(0);
+ });
+
+
+ it('one L1 bot L2 top merge', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,0,0,0], "top":[0,1,0,0], "full":[0,0,0,0]};
+ const free = {"none":[1,0,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(1);
+ expect(result.progress).toBe(2);
+ });
+
+ it('one L1 top L2 bot merge', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,1,0,0], "top":[1,0,0,0], "full":[0,0,0,0]};
+ const free = {"none":[1,0,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(1);
+ expect(result.progress).toBe(2);
+ });
+
+ it('one L2 bot L3 top merge', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,1,0,0], "top":[0,0,1,0], "full":[0,0,0,0]};
+ const free = {"none":[0,1,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(3);
+ expect(result.progress).toBe(2);
+ });
+
+ it('one L2 top L3 bot merge', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,1,0], "top":[0,1,0,0], "full":[0,0,0,0]};
+ const free = {"none":[0,1,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(3);
+ expect(result.progress).toBe(2);
+ });
+
+ it('one L3 bot L4 top merge', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,1,0], "top":[0,0,0,1], "full":[0,0,0,0]};
+ const free = {"none":[0,0,1,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(3);
+ expect(result.progress).toBe(3);
+ });
+
+ it('one L3 top L4 bot merge', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,0,1], "top":[0,0,1,0], "full":[0,0,0,0]};
+ const free = {"none":[0,0,1,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(3);
+ expect(result.progress).toBe(3);
+ });
+
+ it('one L4 top L4 bot merge', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,0,1], "top":[0,0,0,1], "full":[0,0,0,0]};
+ const free = {"none":[0,0,0,1], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(3);
+ expect(result.progress).toBe(4);
+ });
+
+ it('two L1 bot L2 top merges into L4', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[2,0,0,0], "top":[0,2,0,0], "full":[0,0,0,0]};
+ const free = {"none":[2,0,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(3);
+ expect(result.progress).toBe(4);
+ });
+
+ it('two L1 top L2 bot merges into L4', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,2,0,0], "top":[2,0,0,0], "full":[0,0,0,0]};
+ const free = {"none":[2,0,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(3);
+ expect(result.progress).toBe(4);
+ });
+
+
+ it('one L1 bot ignores free L2', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const free = {"none":[1,1,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(0);
+ expect(result.progress).toBe(1);
+ });
+
+ it('one L1 top ignores free L2', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,0,0], "top":[1,0,0,0], "full":[0,0,0,0]};
+ const free = {"none":[1,1,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(0);
+ expect(result.progress).toBe(1);
+ });
+
+ it('one L2 bot ignores free L3', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,1,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const free = {"none":[0,1,1,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(0);
+ expect(result.progress).toBe(1);
+ });
+
+ it('one L2 top ignores free L3', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,0,0], "top":[0,1,0,0], "full":[0,0,0,0]};
+ const free = {"none":[0,1,1,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(0);
+ expect(result.progress).toBe(1);
+ });
+
+ it('one L3 bot ignores free L4', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,1,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const free = {"none":[0,0,1,1], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(0);
+ expect(result.progress).toBe(1);
+ });
+
+ it('one L3 top ignores free L4', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,0,0], "top":[0,0,1,0], "full":[0,0,0,0]};
+ const free = {"none":[0,0,1,1], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(0);
+ expect(result.progress).toBe(1);
+ });
+
+
+ it('gobble locked L4 bot with free', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,0,1], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const free = {"none":[0,0,0,1], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(0);
+ expect(result.progress).toBe(2);
+ });
+
+ it('gobble locked L4 top with free', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,0,0], "top":[0,0,0,1], "full":[0,0,0,0]};
+ const free = {"none":[0,0,0,1], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(0);
+ expect(result.progress).toBe(2);
+ });
+
+ it('gobble locked L4 bots with free', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,0,2], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const free = {"none":[0,0,0,1], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(0);
+ expect(result.progress).toBe(4);
+ pending('Current implementation returns progress 2 instead of 4');
+ });
+
+ it('gobble locked L4 tops with free', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,0,0], "top":[0,0,0,2], "full":[0,0,0,0]};
+ const free = {"none":[0,0,0,1], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(0);
+ expect(result.progress).toBe(4);
+ pending('Current implementation returns progress 2 instead of 4');
+ });
+
+ it('gobble locked L4 bot with bot', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,1,1], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const free = {"none":[0,0,1,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(0);
+ expect(result.progress).toBe(3);
+ pending('Current implementation returns progress 1 instead of 3');
+ });
+
+ it('gobble locked L4 top with top', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,0,0], "top":[0,0,1,1], "full":[0,0,0,0]};
+ const free = {"none":[0,0,1,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(0);
+ expect(result.progress).toBe(3);
+ pending('Current implementation returns progress 1 instead of 3');
+ });
+
+ it('gobble locked L4 bots with bot', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,1,2], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const free = {"none":[0,0,1,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(0);
+ expect(result.progress).toBe(5);
+ pending('Current implementation returns progress 1 instead of 5');
+ });
+
+ it('gobble locked L4 tops with top', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,0,0], "top":[0,0,1,2], "full":[0,0,0,0]};
+ const free = {"none":[0,0,1,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(0);
+ expect(result.progress).toBe(5);
+ pending('Current implementation returns progress 1 instead of 5');
+ });
+
+ it('gobble locked L4 bot with full', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,1,0,1], "top":[0,0,1,0], "full":[0,0,0,0]};
+ const free = {"none":[0,1,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(3);
+ expect(result.progress).toBe(4);
+ });
+
+ it('gobble locked L4 top with full', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,1,0], "top":[0,1,0,1], "full":[0,0,0,0]};
+ const free = {"none":[0,1,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(3);
+ expect(result.progress).toBe(4);
+ });
+
+ it('gobble locked L4 bots with full', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,1,0,2], "top":[0,0,1,0], "full":[0,0,0,0]};
+ const free = {"none":[0,1,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(3);
+ expect(result.progress).toBe(6);
+ });
+
+ it('gobble locked L4 tops with full', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,1,0], "top":[0,1,0,2], "full":[0,0,0,0]};
+ const free = {"none":[0,1,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(3);
+ expect(result.progress).toBe(6);
+ });
+
+
+ it('leave free L4 unmerged', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const free = {"none":[0,0,0,2], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(0);
+ expect(result.progress).toBe(0);
+ });
+
+ it('leave free L4 unmerged in presence of merged key', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,0,1], "top":[0,0,0,1], "full":[0,0,0,0]};
+ const free = {"none":[0,0,0,3], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(3);
+ expect(result.progress).toBe(4);
+ });
+
+ it('leave free L1-4 unmerged', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const free = {"none":[0,0,0,3], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(0);
+ expect(result.progress).toBe(0);
+ });
+
+ it('leave free L1-4 unmerged in presence of merged key', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,0,1], "top":[0,0,0,1], "full":[0,0,0,0]};
+ const free = {"none":[2,2,2,3], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(3);
+ expect(result.progress).toBe(4);
+ });
+
+ it('leave free L1-2, merge free L3 for L4 key', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,0,1], "top":[0,0,0,1], "full":[0,0,0,0]};
+ const free = {"none":[2,2,4,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(3);
+ expect(result.progress).toBe(4);
+ });
+
+ it('leave free L2 bot unmerged, merge free L2 for L4 key', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[2,0,0,1], "top":[0,0,0,1], "full":[0,0,0,0]};
+ const free = {"none":[2,2,1,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(3);
+ expect(result.progress).toBe(6);
+ });
+
+ it('leave free L2 top unmerged, merge free L2 for L4 key', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,0,1], "top":[2,0,0,1], "full":[0,0,0,0]};
+ const free = {"none":[2,2,1,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(3);
+ expect(result.progress).toBe(6);
+ });
+
+
+ it('unmerged top is better than unmerged empty', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,0,1], "top":[1,0,0,1], "full":[0,0,0,0]};
+ const free = {"none":[1,2,1,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(3);
+ expect(result.progress).toBe(5);
+ });
+
+ it('unmerged bot is better than unmerged empty', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,0,0,1], "top":[0,0,0,1], "full":[0,0,0,0]};
+ const free = {"none":[1,2,1,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(3);
+ expect(result.progress).toBe(5);
+ });
+
+ it('one unmerged top is better than two unmerged empties', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,0,1], "top":[1,0,0,1], "full":[0,0,0,0]};
+ const free = {"none":[3,1,1,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(3);
+ expect(result.progress).toBe(5);
+ });
+
+ it('one unmerged bot is better than two unmerged empties', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,0,0,1], "top":[0,0,0,1], "full":[0,0,0,0]};
+ const free = {"none":[3,1,1,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(3);
+ expect(result.progress).toBe(5);
+ });
+ });
+
+ describe('(full)', function() {
+ // full boards gotten from the game and/or comments at https://www.mooingcatguides.com/event-guides/2023-anniversary-event-guide#comments
+ // usually simpler, for which a bruteforce solver is quick
+ it('comment-6152735906 by Stella Yolanda Zonker', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[2,1,0,0], "top":[2,2,1,1], "full":[0,0,0,0]};
+ const free = {"none":[0,0,1,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(0);
+ expect(result.progress).toBe(3);
+ pending('Current implementation returns progress 1 instead of 3');
+ });
+
+ it('comment-6151362161 by szevasz', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,1,0], "top":[1,0,2,1], "full":[0,0,0,0]};
+ const free = {"none":[2,2,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(3);
+ expect(result.progress).toBe(4);
+ });
+
+ it('comment-6149249510 by Muche', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[3,1,0,1], "top":[4,0,0,0], "full":[0,0,0,0]};
+ const free = {"none":[3,0,1,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(4);
+ expect(result.progress).toBe(6);
+ });
+
+ it('comment-6148461376 by Stella Yolanda Zonker', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,2,0,1], "top":[0,0,2,0], "full":[0,0,0,0]};
+ const free = {"none":[1,0,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(3);
+ expect(result.progress).toBe(5);
+ });
+
+ it('comment-6148124724 by Max Barbarian', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,2,1,3], "top":[0,1,2,0], "full":[0,0,0,0]};
+ const free = {"none":[0,1,1,1], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(6);
+ expect(result.progress).toBe(9);
+ });
+
+ it('comment-6153869993 by Stella Yolanda Zonker', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,2,2,1], "top":[3,1,1,0], "full":[0,0,0,0]};
+ const free = {"none":[4,1,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(9);
+ expect(result.progress).toBe(11);
+ });
+
+ it('comment-6155079334 by MarkusS', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,0,0,2], "top":[1,3,2,0], "full":[0,0,0,0]};
+ const free = {"none":[4,0,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(7);
+ expect(result.progress).toBe(11);
+ });
+
+ it('comment-6159840011 by Max Barbarian', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,1,3,2], "top":[1,2,3,0], "full":[0,0,0,0]};
+ const free = {"none":[1,1,0,1], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(6);
+ expect(result.progress).toBe(9);
+ });
+
+ it('tc1', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,3,1,3], "top":[1,0,0,0], "full":[0,0,0,0]};
+ const free = {"none":[1,0,1,1], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(3);
+ expect(result.progress).toBe(9);
+ });
+
+ it('tc2', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[2,0,2,3], "top":[1,0,1,0], "full":[0,0,0,0]};
+ const free = {"none":[4,0,2,1], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(6);
+ expect(result.progress).toBe(12);
+ });
+
+ it('tc3', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[2,0,0,2], "top":[0,0,1,1], "full":[0,0,0,0]};
+ const free = {"none":[4,0,0,2], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(6);
+ expect(result.progress).toBe(9);
+ });
+
+ it('tc4', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,1,0,0], "top":[0,3,1,1], "full":[0,0,0,0]};
+ const free = {"none":[3,1,1,2], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(3);
+ expect(result.progress).toBe(5);
+ });
+
+ it('tc5', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,1,0,0], "top":[3,3,3,2], "full":[0,0,0,0]};
+ const free = {"none":[1,3,2,2], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(3);
+ expect(result.progress).toBe(12);
+ });
+
+ it('tc6', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[2,1,1,0], "top":[5,0,0,0], "full":[0,0,0,0]};
+ const free = {"none":[4,2,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(6);
+ expect(result.progress).toBe(6);
+ });
+
+ it('tc7', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,3,0,0], "top":[0,3,0,0], "full":[0,0,0,0]};
+ const free = {"none":[2,1,1,1], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(3);
+ expect(result.progress).toBe(2);
+ });
+
+ it('tc9', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,1,0,1], "top":[1,0,0,2], "full":[0,0,0,0]};
+ const free = {"none":[1,1,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(1);
+ expect(result.progress).toBe(2);
+ });
+
+ it('tc11', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[3,2,2,1], "top":[5,1,0,0], "full":[0,0,0,0]};
+ const free = {"none":[3,0,1,1], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(9);
+ expect(result.progress).toBe(10);
+ });
+
+ it('tc12', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,0,0,2], "top":[1,3,0,1], "full":[0,0,0,0]};
+ const free = {"none":[0,2,1,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(3);
+ expect(result.progress).toBe(8);
+ });
+
+ it('tc15', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[3,1,2,0], "top":[3,2,0,1], "full":[0,0,0,0]};
+ const free = {"none":[1,1,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(6);
+ expect(result.progress).toBe(7);
+ });
+
+ it('tc16', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,1,0,2], "top":[1,2,1,0], "full":[0,0,0,0]};
+ const free = {"none":[1,0,1,1], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(4);
+ expect(result.progress).toBe(7);
+ });
+
+ it('tc17', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,0,2,2], "top":[0,2,2,1], "full":[0,0,0,0]};
+ const free = {"none":[3,1,0,1], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(9);
+ expect(result.progress).toBe(11);
+ });
+
+ it('tc18', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[3,1,1,0], "top":[2,3,0,1], "full":[0,0,0,0]};
+ const free = {"none":[2,2,1,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(9);
+ expect(result.progress).toBe(9);
+ });
+
+ it('tc19', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,3,0,3], "top":[0,0,2,1], "full":[0,0,0,0]};
+ const free = {"none":[3,0,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(6);
+ expect(result.progress).toBe(13);
+ });
+
+ it('tc20', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[4,0,1,3], "top":[3,0,0,0], "full":[0,0,0,0]};
+ const free = {"none":[2,0,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(3);
+ expect(result.progress).toBe(9);
+ });
+
+ it('tc21', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[3,0,1,2], "top":[0,3,0,0], "full":[0,0,0,0]};
+ const free = {"none":[5,0,3,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(9);
+ expect(result.progress).toBe(11);
+ });
+
+ it('tc22', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,3,0,0], "top":[1,1,2,3], "full":[0,0,0,0]};
+ const free = {"none":[3,0,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(6);
+ expect(result.progress).toBe(12);
+ });
+
+ it('tc23', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[3,2,3,0], "top":[1,2,0,1], "full":[0,0,0,0]};
+ const free = {"none":[3,0,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(9);
+ expect(result.progress).toBe(11);
+ });
+
+ it('tc28', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,1,3,0], "top":[1,0,0,2], "full":[0,0,0,0]};
+ const free = {"none":[4,0,1,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(6);
+ expect(result.progress).toBe(8);
+ });
+
+ it('tc29', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,2,2,2], "top":[3,1,0,1], "full":[0,0,0,0]};
+ const free = {"none":[2,1,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(7);
+ expect(result.progress).toBe(13);
+ });
+
+ it('tc30', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[2,0,2,1], "top":[2,1,1,2], "full":[0,0,0,0]};
+ const free = {"none":[4,0,1,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(9);
+ expect(result.progress).toBe(14);
+ });
+
+ it('tc31', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[3,1,3,1], "top":[2,1,0,1], "full":[0,0,0,0]};
+ const free = {"none":[6,1,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(10);
+ expect(result.progress).toBe(14);
+ });
+
+ it('tc32', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,1,1,0], "top":[3,1,1,1], "full":[0,0,0,0]};
+ const free = {"none":[4,0,0,1], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(7);
+ expect(result.progress).toBe(10);
+ });
+
+ it('tc33', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,2,2,1], "top":[3,2,0,0], "full":[0,0,0,0]};
+ const free = {"none":[4,2,1,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(10);
+ expect(result.progress).toBe(11);
+ });
+
+ it('tc34', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,2,0,1], "top":[1,4,3,0], "full":[0,0,0,0]};
+ const free = {"none":[4,1,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(10);
+ expect(result.progress).toBe(11);
+ });
+
+ it('tc35', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[2,0,2,3], "top":[1,1,0,1], "full":[0,0,0,0]};
+ const free = {"none":[1,1,2,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(6);
+ expect(result.progress).toBe(12);
+ });
+
+ it('tc36', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,0,1,0], "top":[1,2,1,3], "full":[0,0,0,0]};
+ const free = {"none":[1,0,0,1], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(3);
+ expect(result.progress).toBe(9);
+ });
+
+ it('tc38', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,1,1,0], "top":[1,1,0,0], "full":[0,0,0,0]};
+ const free = {"none":[2,3,1,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(6);
+ expect(result.progress).toBe(5);
+ });
+
+ it('tc39', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,0,0], "top":[3,0,0,1], "full":[0,0,0,0]};
+ const free = {"none":[1,1,1,1], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(0);
+ expect(result.progress).toBe(3);
+ });
+
+ it('tc40', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,2,0,1], "top":[4,3,5,0], "full":[0,0,0,0]};
+ const free = {"none":[2,2,4,1], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(9);
+ expect(result.progress).toBe(13);
+ });
+
+ it('tc41', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,3,1,2], "top":[2,2,1,1], "full":[0,0,0,0]};
+ const free = {"none":[3,0,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(7);
+ expect(result.progress).toBe(14);
+ });
+
+ it('tc42', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[3,0,5,0], "top":[2,2,2,0], "full":[0,0,0,0]};
+ const free = {"none":[0,2,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(6);
+ expect(result.progress).toBe(4);
+ });
+
+ it('tc44', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,1,0,0], "top":[4,2,0,0], "full":[0,0,0,0]};
+ const free = {"none":[2,1,2,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(6);
+ expect(result.progress).toBe(5);
+ });
+
+ it('tc45', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,0,2], "top":[1,0,0,0], "full":[0,0,0,0]};
+ const free = {"none":[3,0,2,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(3);
+ expect(result.progress).toBe(5);
+ });
+
+ it('tc46', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[2,1,1,0], "top":[0,3,0,0], "full":[0,0,0,0]};
+ const free = {"none":[4,2,1,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(9);
+ expect(result.progress).toBe(7);
+ });
+
+ it('tc48', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[2,0,1,0], "top":[1,0,1,1], "full":[0,0,0,0]};
+ const free = {"none":[4,2,0,1], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(6);
+ expect(result.progress).toBe(7);
+ });
+
+ it('tc49', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[4,2,5,1], "top":[1,1,1,1], "full":[0,0,0,0]};
+ const free = {"none":[1,2,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(9);
+ expect(result.progress).toBe(11);
+ });
+
+ it('tc50', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,2,0,0], "top":[2,1,2,2], "full":[0,0,0,0]};
+ const free = {"none":[5,1,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(9);
+ expect(result.progress).toBe(12);
+ });
+
+ it('tc51', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[3,2,0,1], "top":[1,1,1,2], "full":[0,0,0,0]};
+ const free = {"none":[4,0,0,1], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(9);
+ expect(result.progress).toBe(14);
+ });
+
+ it('tc52', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,3,1,0], "top":[4,0,1,0], "full":[0,0,0,0]};
+ const free = {"none":[2,1,1,1], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(9);
+ expect(result.progress).toBe(7);
+ });
+
+ it('tc53', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,0,3,2], "top":[2,1,2,1], "full":[0,0,0,0]};
+ const free = {"none":[2,1,2,1], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(12);
+ expect(result.progress).toBe(13);
+ });
+
+ it('tc54', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[2,1,1,0], "top":[0,1,1,2], "full":[0,0,0,0]};
+ const free = {"none":[2,0,1,1], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(7);
+ expect(result.progress).toBe(10);
+ });
+
+ it('tc55', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,2,3,1], "top":[2,1,1,1], "full":[0,0,0,0]};
+ const free = {"none":[4,0,1,1], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(12);
+ expect(result.progress).toBe(14);
+ });
+
+ it('tc56', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[2,2,2,1], "top":[3,1,0,1], "full":[0,0,0,0]};
+ const free = {"none":[5,0,2,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(12);
+ expect(result.progress).toBe(14);
+ });
+ });
+
+ describe('(full perf)', function() {
+ // full boards gotten from the game and/or comments at https://www.mooingcatguides.com/event-guides/2023-anniversary-event-guide#comments
+ // usually more complex, for which a bruteforce solver can be slow
+ it('comment-6153598555 by Centaurus', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[3,3,0,2], "top":[4,3,1,1], "full":[0,0,0,0]};
+ const free = {"none":[1,2,1,1], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(10);
+ expect(result.progress).toBe(11);
+ });
+
+ it('comment-6149964806 by Muche', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,1,1,2], "top":[2,5,1,1], "full":[0,0,0,0]};
+ const free = {"none":[4,0,3,1], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(15);
+ expect(result.progress).toBe(14);
+ });
+
+ it('comment-6147057517 by Rigel Blue', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[4,0,1,2], "top":[2,1,2,1], "full":[0,0,0,0]};
+ const free = {"none":[2,6,1,1], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(12);
+ expect(result.progress).toBe(12);
+ });
+
+ it('tc8', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[3,4,4,1], "top":[1,3,0,1], "full":[0,0,0,0]};
+ const free = {"none":[4,1,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(13);
+ expect(result.progress).toBe(17);
+ });
+
+ it('tc10', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[2,2,4,1], "top":[1,0,2,1], "full":[0,0,0,0]};
+ const free = {"none":[9,0,1,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(12);
+ expect(result.progress).toBe(14);
+ });
+
+ it('tc13', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,1,1,0], "top":[1,1,1,3], "full":[0,0,0,0]};
+ const free = {"none":[5,1,1,1], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(9);
+ expect(result.progress).toBe(12);
+ });
+
+ it('tc14', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[2,3,1,0], "top":[2,0,1,2], "full":[0,0,0,0]};
+ const free = {"none":[2,3,2,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(12);
+ expect(result.progress).toBe(11);
+ });
+
+ it('tc24', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[2,2,3,2], "top":[2,4,2,0], "full":[0,0,0,0]};
+ const free = {"none":[5,3,1,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(18);
+ expect(result.progress).toBe(19);
+ });
+
+ it('tc25', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,1,2,4], "top":[2,2,0,2], "full":[0,0,0,0]};
+ const free = {"none":[3,1,4,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(13);
+ expect(result.progress).toBe(20);
+ });
+
+ it('tc26', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,3,2,1], "top":[2,2,2,1], "full":[0,0,0,0]};
+ const free = {"none":[7,0,4,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(19);
+ expect(result.progress).toBe(16);
+ });
+
+ it('tc27', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[0,1,2,2], "top":[3,3,1,1], "full":[0,0,0,0]};
+ const free = {"none":[3,1,4,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(15);
+ expect(result.progress).toBe(16);
+ });
+
+ it('tc37', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,2,5,3], "top":[1,2,4,0], "full":[0,0,0,0]};
+ const free = {"none":[3,2,2,1], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(18);
+ expect(result.progress).toBe(18);
+ });
+
+ it('tc43', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[2,1,1,2], "top":[1,1,1,1], "full":[0,0,0,0]};
+ const free = {"none":[4,3,2,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(12);
+ expect(result.progress).toBe(13);
+ });
+
+ it('tc47', function() {
+ const locked = {"none":[0,0,0,0], "bottom":[1,6,2,4], "top":[1,2,3,3], "full":[0,0,0,0]};
+ const free = {"none":[6,6,0,0], "bottom":[0,0,0,0], "top":[0,0,0,0], "full":[0,0,0,0]};
+ const result = mergerGame.solver1(locked, free);
+
+ expect(result.keys).toBe(21);
+ expect(result.progress).toBe(29);
+ });
+ });
+ });
+});
diff --git a/js/web/popgame/test/popgame.js b/js/web/popgame/test/popgame.js
new file mode 100644
index 000000000..3e0f8f8b3
--- /dev/null
+++ b/js/web/popgame/test/popgame.js
@@ -0,0 +1,69 @@
+describe(tt.getModuleSuiteName(), function() {
+ describe('tracking initialization', function() {
+ // reported in https://discord.com/channels/577475401144598539/950390739437760523/1184507486787612742
+ // and https://discord.com/channels/577475401144598539/950390739437760523/1184517848706596988
+ afterAll(function() {
+ localStorage.removeItem('popgameTracking');
+ });
+
+ it('if no stored value', function() {
+ localStorage.removeItem('popgameTracking');
+ Popgame.trackingInit();
+
+ expect(Popgame.tracking).toBeTruthy();
+ expect(Popgame.tracking.start).toBeTruthy();
+ expect(Popgame.tracking.start.total).toBe(0);
+ expect(Popgame.tracking.start.grandPrize).toBe(0);
+ expect(Popgame.tracking.afterPop).toBeTruthy();
+ expect(Popgame.tracking.afterPop.total).toBe(0);
+ expect(Popgame.tracking.afterPop.grandPrize).toBe(0);
+ expect(Popgame.tracking.leftOnBoard).toBeTruthy();
+ expect(Popgame.tracking.leftOnBoard.grandPrize).toBe(0);
+ });
+
+ it('if stored value is invalid', function() {
+ localStorage.setItem('popgameTracking','foobar');
+ Popgame.trackingInit();
+
+ expect(Popgame.tracking).toBeTruthy();
+ expect(Popgame.tracking.start).toBeTruthy();
+ expect(Popgame.tracking.start.total).toBe(0);
+ expect(Popgame.tracking.start.grandPrize).toBe(0);
+ expect(Popgame.tracking.afterPop).toBeTruthy();
+ expect(Popgame.tracking.afterPop.total).toBe(0);
+ expect(Popgame.tracking.afterPop.grandPrize).toBe(0);
+ expect(Popgame.tracking.leftOnBoard).toBeTruthy();
+ expect(Popgame.tracking.leftOnBoard.grandPrize).toBe(0);
+ });
+
+ it('if stored value is v1', function() {
+ localStorage.setItem('popgameTracking','{"start":{"total":3,"grandPrize":5 }, "afterPop":{"total":7,"grandPrize":11} }');
+ Popgame.trackingInit();
+
+ expect(Popgame.tracking).toBeTruthy();
+ expect(Popgame.tracking.start).toBeTruthy();
+ expect(Popgame.tracking.start.total).toBe(0);
+ expect(Popgame.tracking.start.grandPrize).toBe(0);
+ expect(Popgame.tracking.afterPop).toBeTruthy();
+ expect(Popgame.tracking.afterPop.total).toBe(0);
+ expect(Popgame.tracking.afterPop.grandPrize).toBe(0);
+ expect(Popgame.tracking.leftOnBoard).toBeTruthy();
+ expect(Popgame.tracking.leftOnBoard.grandPrize).toBe(0);
+ });
+
+ it('if stored value is v2', function() {
+ localStorage.setItem('popgameTracking','{"start":{"total":3,"grandPrize":5}, "afterPop":{"total":7,"grandPrize":11}, "leftOnBoard":{"grandPrize":13} }');
+ Popgame.trackingInit();
+
+ expect(Popgame.tracking).toBeTruthy();
+ expect(Popgame.tracking.start).toBeTruthy();
+ expect(Popgame.tracking.start.total).toBe(3);
+ expect(Popgame.tracking.start.grandPrize).toBe(5);
+ expect(Popgame.tracking.afterPop).toBeTruthy();
+ expect(Popgame.tracking.afterPop.total).toBe(7);
+ expect(Popgame.tracking.afterPop.grandPrize).toBe(11);
+ expect(Popgame.tracking.leftOnBoard).toBeTruthy();
+ expect(Popgame.tracking.leftOnBoard.grandPrize).toBe(13);
+ });
+ });
+});
diff --git a/test.html b/test.html
new file mode 100644
index 000000000..ad9ceac48
--- /dev/null
+++ b/test.html
@@ -0,0 +1,39 @@
+
+
+
+
+ FoE Helper Spec Runner
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/boot1.js b/test/boot1.js
new file mode 100644
index 000000000..f69dac606
--- /dev/null
+++ b/test/boot1.js
@@ -0,0 +1,129 @@
+/*
+Copyright (c) 2008-2023 Pivotal Labs
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+/**
+ This file finishes 'booting' Jasmine, performing all of the necessary
+ initialization before executing the loaded environment and all of a project's
+ specs. This file should be loaded after `boot0.js` but before any project
+ source files or spec files are loaded. Thus this file can also be used to
+ customize Jasmine for a project.
+
+ If a project is using Jasmine via the standalone distribution, this file can
+ be customized directly. If you only wish to configure the Jasmine env, you
+ can load another file that calls `jasmine.getEnv().configure({...})`
+ after `boot0.js` is loaded and before this file is loaded.
+ */
+
+(function() {
+ const env = jasmine.getEnv();
+
+ /**
+ * ## Runner Parameters
+ *
+ * More browser specific code - wrap the query string in an object and to allow for getting/setting parameters from the runner user interface.
+ */
+
+ const queryString = new jasmine.QueryString({
+ getWindowLocation: function() {
+ return window.location;
+ }
+ });
+
+ const filterSpecs = !!queryString.getParam('spec');
+
+ const config = {
+ stopOnSpecFailure: queryString.getParam('stopOnSpecFailure'),
+ stopSpecOnExpectationFailure: queryString.getParam(
+ 'stopSpecOnExpectationFailure'
+ ),
+ hideDisabled: queryString.getParam('hideDisabled')
+ };
+
+ const random = queryString.getParam('random');
+
+ if (random !== undefined && random !== '') {
+ config.random = random;
+ }
+
+ const seed = queryString.getParam('seed');
+ if (seed) {
+ config.seed = seed;
+ }
+
+ /**
+ * ## Reporters
+ * The `HtmlReporter` builds all of the HTML UI for the runner page. This reporter paints the dots, stars, and x's for specs, as well as all spec names and all failures (if any).
+ */
+ const htmlReporter = new jasmine.HtmlReporter({
+ env: env,
+ navigateWithNewParam: function(key, value) {
+ return queryString.navigateWithNewParam(key, value);
+ },
+ addToExistingQueryString: function(key, value) {
+ return queryString.fullStringWithNewParam(key, value);
+ },
+ getContainer: function() {
+ return document.body;
+ },
+ createElement: function() {
+ return document.createElement.apply(document, arguments);
+ },
+ createTextNode: function() {
+ return document.createTextNode.apply(document, arguments);
+ },
+ timer: new jasmine.Timer(),
+ filterSpecs: filterSpecs
+ });
+
+ /**
+ * The `jsApiReporter` also receives spec results, and is used by any environment that needs to extract the results from JavaScript.
+ */
+ env.addReporter(jsApiReporter);
+ env.addReporter(htmlReporter);
+
+ /**
+ * Filter which specs will be run by matching the start of the full name against the `spec` query param.
+ */
+ const specFilter = new jasmine.HtmlSpecFilter({
+ filterString: function() {
+ return queryString.getParam('spec');
+ }
+ });
+
+ config.specFilter = function(spec) {
+ return specFilter.matches(spec.getFullName());
+ };
+
+ env.configure(config);
+
+ /**
+ * ## Execution
+ *
+ * Add an event listener for 'foe-helper#loaded' event.
+ * Originally used 'window.load' event was not reliable - it could be fired earlier than FoE Helper finished loading.
+ * Run all of the loaded specs. This includes initializing the `HtmlReporter` instance and then executing the loaded Jasmine environment. All of this will happen after all of the specs are loaded.
+ */
+ window.addEventListener('foe-helper#loaded', function () {
+ htmlReporter.initialize();
+ env.execute();
+ }, { capture: false, once: true, passive: true });
+})();
diff --git a/test/testing.md b/test/testing.md
new file mode 100644
index 000000000..7ed6c1892
--- /dev/null
+++ b/test/testing.md
@@ -0,0 +1,107 @@
+# Test environment
+
+Tests are using [Jasmine framework](https://jasmine.github.io).
+
+Tests are run by [/test.html](#testhtml), accessed directly within the extension.
+For example, run in the console (on a loaded game page): `copy(extUrl + 'test.html')`, paste copied link into a bookmark.
+
+This has several consequences:
+
+- [Injector](../js/inject.js) runs within the extension context, which is expected and required.
+- Document, the target of injecting, is also within the extension context as opposed to content context.
+ The running code has elevated privileges not present in a release environment.
+- The extension code depends on *jQuery*. In a release environment, *jQuery* is loaded by the game itself, in a test environment, [our own version](../vendor/jQuery/jquery.min.js) is loaded.
+- *Injector* has had a hard dependency on [languages module](../js/web/_languages/js/_languages.js).
+ As it is also one of injected modules and can't be loaded twice, the dependency is soft now.
+ In a test environment, *injector* falls back to a default, later loads *languages* as usual.
+ In a release environment, *languages* is loaded within the extension context with injector utilizing it, later it is loaded again within the content context as usual.
+- The code (tests as well modules under test), having extension privileges, has access to actual local storage and databases of a release environment - the same as *injector* and [/background.js](../background.js) have.
+ Extra care needs to be taken when testing stuff that touches storage - code can access information not present in release, and can alter information presently used in release - including, but not limited to, extension information, GUI language preference, and [Alerts](../js/web/alerts/js/alerts.js).
+
+# Files & Folders organisation
+
+## [/test.html](../test.html)
+
+The tests specs runner. Loads *Jasmine*, *FoE Helper* modules, test modules.
+Tests are now run on `foe-helper#loaded` event, as the original `load` event is unreliable for this purpose - frequently `load` is fired earlier than `foe-helper#loaded`, resulting in unresolved references.
+Currently test modules are linked directly here. See [TODO](#known-limitations--issues--things-to-do) below.
+
+## [/vendor/jasmine](../vendor/jasmine/)
+
+Jasmine framework files.
+Should a file need to be altered, it is copied and altered into [/test](#test), with a copy left here for reference.
+
+## [/test](./)
+
+Common and shared files.
+
+### [testtools.js](testtools.js), [testtoolsspecs.js](testtoolsspecs.js)
+
+Test tools / utilities with their own tests.
+
+## */js/web/${modulename}/test/*
+
+Test specs for a particular module.
+
+From the topmost suite name it should be easy to identify/locate the file the suite is in.
+`tt.getModuleSuiteName()` was implemented to facilitate that, returns the filename it is used in. This assumes there is an association between test module filename and the module it belongs to.
+Thus the recommended filename is `modulename.js`. Also see [TODO](#known-limitations--issues--things-to-do) below.
+
+# Known limitations / issues / things to do
+
+- Test modules are hard-linked in [/test.html](#testhtml).
+
+ Add loading them dynamically, referencing the same [/js/internal.json](../js/internal.json) as *injector* is using.
+ Planned supported test module names are `${modulename}.js` and `${modulename}N.js`, where *N* is a number, starting from *1*.
+ Numbers need to increase sequentially, that is, *(1,2,3,...,k)*. After the missing one *(k+1)*, further ones will not be discovered / loaded.
+ This test module splitting will allow for more flexible maintenance of larger files.
+
+- Test environment reset.
+
+ The usual paradigm is that each test suite prepares environment to its requirements in `beforeAll`/`beforeEach` and cleans up afterwards in `afterAll`/`afterEach`.
+ The challenge here is that if a module accesses another module (e.g. most modules refer to *MainParser*, *BlueGalaxy* refers to *Productions*, *InfoBoard* refers to *GuildFights* and *Discord*), it needs to reset that one as well.
+ Currently, this would be still achievable by implementing and calling all appropriate module resets.
+ However, after adding support for loading and simulating requests and responses (see below), this might no longer be easily done.
+ A response intended to test funcionality of one module may change the state of another module, possibly triggering unrelated errors.
+ The planned solution to this is to implement helper-wide reset - a global reset utility function which calls all appropriate (i.e. having a tag requested by the resetter) modules' reset function.
+ This would make modules unrelated to current test responsible for proper reaction only from a known zero state, not from all possible states created as a side-effect in various other tests.
+
+- Test environment (non-)reset in interactive mode.
+
+ Investigate ways to not perform the cleanup of `after*`, so the environment stays intact after a test (if it was the last/sole one to run).
+ This could facilitate interactively diagnosing failed tests, and develop new tests, including GUI-related tests.
+
+- Support for loading data files.
+
+ Currently, it should be possible to load data files ad-hoc with `fetch`. Formalize this.
+ Shared data files will be in `/test/data/`, module's private data files will be in `/js/web/${modulename}/test/data/`.
+ This will allow separating large (input) data from the test itself,
+
+- Support for large result tests.
+
+ In addition to simple tests (small input, small result) and semi-complex tests (large input, small result), large result tests support could be useful to have.
+ These kinds of tests could be characterised by the output result that is created either procedurally from small input, or by transforming a large input.
+ This output then creates the baseline the future results are compared against.
+ The focus is less on the actual results, and more on detecting the changed result and patterns of differences.
+ The plan is to add tools for handling the large result during testing (loading and comparing as objects),
+ and after testing (saving the result(s) in file(s) suitable for comparing with external line-based diff tool, and moving it back into repository - replacing the old result).
+
+- Support for simulating server requests and responses.
+
+ Add loading a specified data file and simulate it arriving from the server, triggering all appropriate registered handlers.
+
+- In the future, the *FoE Helper extension* release procedure will need to be altered to not include test files in the released archive.
+
+ Presently and in the short term, I don't expect the effect to matter much (total test files size is about 4% of the total (uncompressed) extension size.
+ In the long term, especially after formalized support for loading of data files (see above), the effect will be substantial - I expect e.g. the full 30MB *CityEntities* data file to be included and used for testing.
+
+- Errors unrelated to running tests.
+
+ When the browser environment was set up incorrectly and the background service worker failed, attempts at communication of *Alerts* with it were causing errors that affected running tests.
+ As a quick workaround, the errors were degraded to console logs to not affect the results of tests.
+
+- GUI-related tests.
+
+ A test can create and test GUI elements.
+ However, their automated testing is limited to DOM API access only. Evaluating the visual result and rendering of all applied css styles is currently possible manually only.
+ The options for automating it and possible challenges of compatibility across browsers/OSes have not been explored.
diff --git a/test/testtools.js b/test/testtools.js
new file mode 100644
index 000000000..c48cdbd00
--- /dev/null
+++ b/test/testtools.js
@@ -0,0 +1,24 @@
+var tt = {
+ /**
+ * Returns full filename (including parent folders) of the file currently being executed
+ *
+ * @returns {string}
+ */
+ getFilename() {
+ const frames = new jasmine.StackTrace(new Error()).frames;
+ return frames[1] ? frames[1].file : frames.join('|');
+ },
+
+ /**
+ * Returns filename (without extension) of the file currently being executed
+ * usually used to uniquely identify module suite name
+ *
+ * @returns {string}
+ */
+ getModuleSuiteName() {
+ const frames = new jasmine.StackTrace(new Error()).frames;
+ const filename = frames[1] && frames[1].file || '';
+ const result = filename.match(/.*\/(.*)\.[^\.]+$/);
+ return result ? result[1] : filename;
+ }
+};
diff --git a/test/testtoolsspecs.js b/test/testtoolsspecs.js
new file mode 100644
index 000000000..f0ed0c5c2
--- /dev/null
+++ b/test/testtoolsspecs.js
@@ -0,0 +1,9 @@
+describe('test tools', function() {
+ it('getFilename', function() {
+ expect(tt.getFilename()).toBe(extUrl + 'test/testtoolsspecs.js');
+ });
+
+ it('getModuleSuiteName', function() {
+ expect(tt.getModuleSuiteName()).toBe('testtoolsspecs');
+ });
+});
diff --git a/vendor/jasmine/boot0.js b/vendor/jasmine/boot0.js
new file mode 100644
index 000000000..b053af10d
--- /dev/null
+++ b/vendor/jasmine/boot0.js
@@ -0,0 +1,64 @@
+/*
+Copyright (c) 2008-2023 Pivotal Labs
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+/**
+ This file starts the process of "booting" Jasmine. It initializes Jasmine,
+ makes its globals available, and creates the env. This file should be loaded
+ after `jasmine.js` and `jasmine_html.js`, but before `boot1.js` or any project
+ source files or spec files are loaded.
+ */
+(function() {
+ const jasmineRequire = window.jasmineRequire || require('./jasmine.js');
+
+ /**
+ * ## Require & Instantiate
+ *
+ * Require Jasmine's core files. Specifically, this requires and attaches all of Jasmine's code to the `jasmine` reference.
+ */
+ const jasmine = jasmineRequire.core(jasmineRequire),
+ global = jasmine.getGlobal();
+ global.jasmine = jasmine;
+
+ /**
+ * Since this is being run in a browser and the results should populate to an HTML page, require the HTML-specific Jasmine code, injecting the same reference.
+ */
+ jasmineRequire.html(jasmine);
+
+ /**
+ * Create the Jasmine environment. This is used to run all specs in a project.
+ */
+ const env = jasmine.getEnv();
+
+ /**
+ * ## The Global Interface
+ *
+ * Build up the functions that will be exposed as the Jasmine public interface. A project can customize, rename or alias any of these functions as desired, provided the implementation remains unchanged.
+ */
+ const jasmineInterface = jasmineRequire.interface(jasmine, env);
+
+ /**
+ * Add all of the Jasmine global/public interface to the global scope, so a project can use the public interface directly. For example, calling `describe` in specs instead of `jasmine.getEnv().describe`.
+ */
+ for (const property in jasmineInterface) {
+ global[property] = jasmineInterface[property];
+ }
+})();
diff --git a/vendor/jasmine/boot1.js b/vendor/jasmine/boot1.js
new file mode 100644
index 000000000..b06f7d8cc
--- /dev/null
+++ b/vendor/jasmine/boot1.js
@@ -0,0 +1,132 @@
+/*
+Copyright (c) 2008-2023 Pivotal Labs
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+/**
+ This file finishes 'booting' Jasmine, performing all of the necessary
+ initialization before executing the loaded environment and all of a project's
+ specs. This file should be loaded after `boot0.js` but before any project
+ source files or spec files are loaded. Thus this file can also be used to
+ customize Jasmine for a project.
+
+ If a project is using Jasmine via the standalone distribution, this file can
+ be customized directly. If you only wish to configure the Jasmine env, you
+ can load another file that calls `jasmine.getEnv().configure({...})`
+ after `boot0.js` is loaded and before this file is loaded.
+ */
+
+(function() {
+ const env = jasmine.getEnv();
+
+ /**
+ * ## Runner Parameters
+ *
+ * More browser specific code - wrap the query string in an object and to allow for getting/setting parameters from the runner user interface.
+ */
+
+ const queryString = new jasmine.QueryString({
+ getWindowLocation: function() {
+ return window.location;
+ }
+ });
+
+ const filterSpecs = !!queryString.getParam('spec');
+
+ const config = {
+ stopOnSpecFailure: queryString.getParam('stopOnSpecFailure'),
+ stopSpecOnExpectationFailure: queryString.getParam(
+ 'stopSpecOnExpectationFailure'
+ ),
+ hideDisabled: queryString.getParam('hideDisabled')
+ };
+
+ const random = queryString.getParam('random');
+
+ if (random !== undefined && random !== '') {
+ config.random = random;
+ }
+
+ const seed = queryString.getParam('seed');
+ if (seed) {
+ config.seed = seed;
+ }
+
+ /**
+ * ## Reporters
+ * The `HtmlReporter` builds all of the HTML UI for the runner page. This reporter paints the dots, stars, and x's for specs, as well as all spec names and all failures (if any).
+ */
+ const htmlReporter = new jasmine.HtmlReporter({
+ env: env,
+ navigateWithNewParam: function(key, value) {
+ return queryString.navigateWithNewParam(key, value);
+ },
+ addToExistingQueryString: function(key, value) {
+ return queryString.fullStringWithNewParam(key, value);
+ },
+ getContainer: function() {
+ return document.body;
+ },
+ createElement: function() {
+ return document.createElement.apply(document, arguments);
+ },
+ createTextNode: function() {
+ return document.createTextNode.apply(document, arguments);
+ },
+ timer: new jasmine.Timer(),
+ filterSpecs: filterSpecs
+ });
+
+ /**
+ * The `jsApiReporter` also receives spec results, and is used by any environment that needs to extract the results from JavaScript.
+ */
+ env.addReporter(jsApiReporter);
+ env.addReporter(htmlReporter);
+
+ /**
+ * Filter which specs will be run by matching the start of the full name against the `spec` query param.
+ */
+ const specFilter = new jasmine.HtmlSpecFilter({
+ filterString: function() {
+ return queryString.getParam('spec');
+ }
+ });
+
+ config.specFilter = function(spec) {
+ return specFilter.matches(spec.getFullName());
+ };
+
+ env.configure(config);
+
+ /**
+ * ## Execution
+ *
+ * Replace the browser window's `onload`, ensure it's called, and then run all of the loaded specs. This includes initializing the `HtmlReporter` instance and then executing the loaded Jasmine environment. All of this will happen after all of the specs are loaded.
+ */
+ const currentWindowOnload = window.onload;
+
+ window.onload = function() {
+ if (currentWindowOnload) {
+ currentWindowOnload();
+ }
+ htmlReporter.initialize();
+ env.execute();
+ };
+})();
diff --git a/vendor/jasmine/jasmine-html.js b/vendor/jasmine/jasmine-html.js
new file mode 100644
index 000000000..e35a9e7c5
--- /dev/null
+++ b/vendor/jasmine/jasmine-html.js
@@ -0,0 +1,963 @@
+/*
+Copyright (c) 2008-2023 Pivotal Labs
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+// eslint-disable-next-line no-var
+var jasmineRequire = window.jasmineRequire || require('./jasmine.js');
+
+jasmineRequire.html = function(j$) {
+ j$.ResultsNode = jasmineRequire.ResultsNode();
+ j$.HtmlReporter = jasmineRequire.HtmlReporter(j$);
+ j$.QueryString = jasmineRequire.QueryString();
+ j$.HtmlSpecFilter = jasmineRequire.HtmlSpecFilter();
+};
+
+jasmineRequire.HtmlReporter = function(j$) {
+ function ResultsStateBuilder() {
+ this.topResults = new j$.ResultsNode({}, '', null);
+ this.currentParent = this.topResults;
+ this.specsExecuted = 0;
+ this.failureCount = 0;
+ this.pendingSpecCount = 0;
+ }
+
+ ResultsStateBuilder.prototype.suiteStarted = function(result) {
+ this.currentParent.addChild(result, 'suite');
+ this.currentParent = this.currentParent.last();
+ };
+
+ ResultsStateBuilder.prototype.suiteDone = function(result) {
+ this.currentParent.updateResult(result);
+ if (this.currentParent !== this.topResults) {
+ this.currentParent = this.currentParent.parent;
+ }
+
+ if (result.status === 'failed') {
+ this.failureCount++;
+ }
+ };
+
+ ResultsStateBuilder.prototype.specStarted = function(result) {};
+
+ ResultsStateBuilder.prototype.specDone = function(result) {
+ this.currentParent.addChild(result, 'spec');
+
+ if (result.status !== 'excluded') {
+ this.specsExecuted++;
+ }
+
+ if (result.status === 'failed') {
+ this.failureCount++;
+ }
+
+ if (result.status == 'pending') {
+ this.pendingSpecCount++;
+ }
+ };
+
+ ResultsStateBuilder.prototype.jasmineDone = function(result) {
+ if (result.failedExpectations) {
+ this.failureCount += result.failedExpectations.length;
+ }
+ };
+
+ function HtmlReporter(options) {
+ function config() {
+ return (options.env && options.env.configuration()) || {};
+ }
+
+ const getContainer = options.getContainer;
+ const createElement = options.createElement;
+ const createTextNode = options.createTextNode;
+ const navigateWithNewParam = options.navigateWithNewParam || function() {};
+ const addToExistingQueryString =
+ options.addToExistingQueryString || defaultQueryString;
+ const filterSpecs = options.filterSpecs;
+ let htmlReporterMain;
+ let symbols;
+ const deprecationWarnings = [];
+ const failures = [];
+
+ this.initialize = function() {
+ clearPrior();
+ htmlReporterMain = createDom(
+ 'div',
+ { className: 'jasmine_html-reporter' },
+ createDom(
+ 'div',
+ { className: 'jasmine-banner' },
+ createDom('a', {
+ className: 'jasmine-title',
+ href: 'http://jasmine.github.io/',
+ target: '_blank'
+ }),
+ createDom('span', { className: 'jasmine-version' }, j$.version)
+ ),
+ createDom('ul', { className: 'jasmine-symbol-summary' }),
+ createDom('div', { className: 'jasmine-alert' }),
+ createDom(
+ 'div',
+ { className: 'jasmine-results' },
+ createDom('div', { className: 'jasmine-failures' })
+ )
+ );
+ getContainer().appendChild(htmlReporterMain);
+ };
+
+ let totalSpecsDefined;
+ this.jasmineStarted = function(options) {
+ totalSpecsDefined = options.totalSpecsDefined || 0;
+ };
+
+ const summary = createDom('div', { className: 'jasmine-summary' });
+
+ const stateBuilder = new ResultsStateBuilder();
+
+ this.suiteStarted = function(result) {
+ stateBuilder.suiteStarted(result);
+ };
+
+ this.suiteDone = function(result) {
+ stateBuilder.suiteDone(result);
+
+ if (result.status === 'failed') {
+ failures.push(failureDom(result));
+ }
+ addDeprecationWarnings(result, 'suite');
+ };
+
+ this.specStarted = function(result) {
+ stateBuilder.specStarted(result);
+ };
+
+ this.specDone = function(result) {
+ stateBuilder.specDone(result);
+
+ if (noExpectations(result)) {
+ const noSpecMsg = "Spec '" + result.fullName + "' has no expectations.";
+ if (result.status === 'failed') {
+ console.error(noSpecMsg);
+ } else {
+ console.warn(noSpecMsg);
+ }
+ }
+
+ if (!symbols) {
+ symbols = find('.jasmine-symbol-summary');
+ }
+
+ symbols.appendChild(
+ createDom('li', {
+ className: this.displaySpecInCorrectFormat(result),
+ id: 'spec_' + result.id,
+ title: result.fullName
+ })
+ );
+
+ if (result.status === 'failed') {
+ failures.push(failureDom(result));
+ }
+
+ addDeprecationWarnings(result, 'spec');
+ };
+
+ this.displaySpecInCorrectFormat = function(result) {
+ return noExpectations(result) && result.status === 'passed'
+ ? 'jasmine-empty'
+ : this.resultStatus(result.status);
+ };
+
+ this.resultStatus = function(status) {
+ if (status === 'excluded') {
+ return config().hideDisabled
+ ? 'jasmine-excluded-no-display'
+ : 'jasmine-excluded';
+ }
+ return 'jasmine-' + status;
+ };
+
+ this.jasmineDone = function(doneResult) {
+ stateBuilder.jasmineDone(doneResult);
+ const banner = find('.jasmine-banner');
+ const alert = find('.jasmine-alert');
+ const order = doneResult && doneResult.order;
+
+ alert.appendChild(
+ createDom(
+ 'span',
+ { className: 'jasmine-duration' },
+ 'finished in ' + doneResult.totalTime / 1000 + 's'
+ )
+ );
+
+ banner.appendChild(optionsMenu(config()));
+
+ if (stateBuilder.specsExecuted < totalSpecsDefined) {
+ const skippedMessage =
+ 'Ran ' +
+ stateBuilder.specsExecuted +
+ ' of ' +
+ totalSpecsDefined +
+ ' specs - run all';
+ // include window.location.pathname to fix issue with karma-jasmine-html-reporter in angular: see https://github.com/jasmine/jasmine/issues/1906
+ const skippedLink =
+ (window.location.pathname || '') +
+ addToExistingQueryString('spec', '');
+ alert.appendChild(
+ createDom(
+ 'span',
+ { className: 'jasmine-bar jasmine-skipped' },
+ createDom(
+ 'a',
+ { href: skippedLink, title: 'Run all specs' },
+ skippedMessage
+ )
+ )
+ );
+ }
+ let statusBarMessage = '';
+ let statusBarClassName = 'jasmine-overall-result jasmine-bar ';
+ const globalFailures =
+ (doneResult && doneResult.failedExpectations) || [];
+ const failed = stateBuilder.failureCount + globalFailures.length > 0;
+
+ if (totalSpecsDefined > 0 || failed) {
+ statusBarMessage +=
+ pluralize('spec', stateBuilder.specsExecuted) +
+ ', ' +
+ pluralize('failure', stateBuilder.failureCount);
+ if (stateBuilder.pendingSpecCount) {
+ statusBarMessage +=
+ ', ' + pluralize('pending spec', stateBuilder.pendingSpecCount);
+ }
+ }
+
+ if (doneResult.overallStatus === 'passed') {
+ statusBarClassName += ' jasmine-passed ';
+ } else if (doneResult.overallStatus === 'incomplete') {
+ statusBarClassName += ' jasmine-incomplete ';
+ statusBarMessage =
+ 'Incomplete: ' +
+ doneResult.incompleteReason +
+ ', ' +
+ statusBarMessage;
+ } else {
+ statusBarClassName += ' jasmine-failed ';
+ }
+
+ let seedBar;
+ if (order && order.random) {
+ seedBar = createDom(
+ 'span',
+ { className: 'jasmine-seed-bar' },
+ ', randomized with seed ',
+ createDom(
+ 'a',
+ {
+ title: 'randomized with seed ' + order.seed,
+ href: seedHref(order.seed)
+ },
+ order.seed
+ )
+ );
+ }
+
+ alert.appendChild(
+ createDom(
+ 'span',
+ { className: statusBarClassName },
+ statusBarMessage,
+ seedBar
+ )
+ );
+
+ const errorBarClassName = 'jasmine-bar jasmine-errored';
+ const afterAllMessagePrefix = 'AfterAll ';
+
+ for (let i = 0; i < globalFailures.length; i++) {
+ alert.appendChild(
+ createDom(
+ 'span',
+ { className: errorBarClassName },
+ globalFailureMessage(globalFailures[i])
+ )
+ );
+ }
+
+ function globalFailureMessage(failure) {
+ if (failure.globalErrorType === 'load') {
+ const prefix = 'Error during loading: ' + failure.message;
+
+ if (failure.filename) {
+ return (
+ prefix + ' in ' + failure.filename + ' line ' + failure.lineno
+ );
+ } else {
+ return prefix;
+ }
+ } else if (failure.globalErrorType === 'afterAll') {
+ return afterAllMessagePrefix + failure.message;
+ } else {
+ return failure.message;
+ }
+ }
+
+ addDeprecationWarnings(doneResult);
+
+ for (let i = 0; i < deprecationWarnings.length; i++) {
+ const children = [];
+ let context;
+
+ switch (deprecationWarnings[i].runnableType) {
+ case 'spec':
+ context = '(in spec: ' + deprecationWarnings[i].runnableName + ')';
+ break;
+ case 'suite':
+ context = '(in suite: ' + deprecationWarnings[i].runnableName + ')';
+ break;
+ default:
+ context = '';
+ }
+
+ deprecationWarnings[i].message.split('\n').forEach(function(line) {
+ children.push(line);
+ children.push(createDom('br'));
+ });
+
+ children[0] = 'DEPRECATION: ' + children[0];
+ children.push(context);
+
+ if (deprecationWarnings[i].stack) {
+ children.push(createExpander(deprecationWarnings[i].stack));
+ }
+
+ alert.appendChild(
+ createDom(
+ 'span',
+ { className: 'jasmine-bar jasmine-warning' },
+ children
+ )
+ );
+ }
+
+ const results = find('.jasmine-results');
+ results.appendChild(summary);
+
+ summaryList(stateBuilder.topResults, summary);
+
+ if (failures.length) {
+ alert.appendChild(
+ createDom(
+ 'span',
+ { className: 'jasmine-menu jasmine-bar jasmine-spec-list' },
+ createDom('span', {}, 'Spec List | '),
+ createDom(
+ 'a',
+ { className: 'jasmine-failures-menu', href: '#' },
+ 'Failures'
+ )
+ )
+ );
+ alert.appendChild(
+ createDom(
+ 'span',
+ { className: 'jasmine-menu jasmine-bar jasmine-failure-list' },
+ createDom(
+ 'a',
+ { className: 'jasmine-spec-list-menu', href: '#' },
+ 'Spec List'
+ ),
+ createDom('span', {}, ' | Failures ')
+ )
+ );
+
+ find('.jasmine-failures-menu').onclick = function() {
+ setMenuModeTo('jasmine-failure-list');
+ return false;
+ };
+ find('.jasmine-spec-list-menu').onclick = function() {
+ setMenuModeTo('jasmine-spec-list');
+ return false;
+ };
+
+ setMenuModeTo('jasmine-failure-list');
+
+ const failureNode = find('.jasmine-failures');
+ for (let i = 0; i < failures.length; i++) {
+ failureNode.appendChild(failures[i]);
+ }
+ }
+ };
+
+ return this;
+
+ function failureDom(result) {
+ const failure = createDom(
+ 'div',
+ { className: 'jasmine-spec-detail jasmine-failed' },
+ failureDescription(result, stateBuilder.currentParent),
+ createDom('div', { className: 'jasmine-messages' })
+ );
+ const messages = failure.childNodes[1];
+
+ for (let i = 0; i < result.failedExpectations.length; i++) {
+ const expectation = result.failedExpectations[i];
+ messages.appendChild(
+ createDom(
+ 'div',
+ { className: 'jasmine-result-message' },
+ expectation.message
+ )
+ );
+ messages.appendChild(
+ createDom(
+ 'div',
+ { className: 'jasmine-stack-trace' },
+ expectation.stack
+ )
+ );
+ }
+
+ if (result.failedExpectations.length === 0) {
+ messages.appendChild(
+ createDom(
+ 'div',
+ { className: 'jasmine-result-message' },
+ 'Spec has no expectations'
+ )
+ );
+ }
+
+ if (result.debugLogs) {
+ messages.appendChild(debugLogTable(result.debugLogs));
+ }
+
+ return failure;
+ }
+
+ function debugLogTable(debugLogs) {
+ const tbody = createDom('tbody');
+
+ debugLogs.forEach(function(entry) {
+ tbody.appendChild(
+ createDom(
+ 'tr',
+ {},
+ createDom('td', {}, entry.timestamp.toString()),
+ createDom('td', {}, entry.message)
+ )
+ );
+ });
+
+ return createDom(
+ 'div',
+ { className: 'jasmine-debug-log' },
+ createDom(
+ 'div',
+ { className: 'jasmine-debug-log-header' },
+ 'Debug logs'
+ ),
+ createDom(
+ 'table',
+ {},
+ createDom(
+ 'thead',
+ {},
+ createDom(
+ 'tr',
+ {},
+ createDom('th', {}, 'Time (ms)'),
+ createDom('th', {}, 'Message')
+ )
+ ),
+ tbody
+ )
+ );
+ }
+
+ function summaryList(resultsTree, domParent) {
+ let specListNode;
+ for (let i = 0; i < resultsTree.children.length; i++) {
+ const resultNode = resultsTree.children[i];
+ if (filterSpecs && !hasActiveSpec(resultNode)) {
+ continue;
+ }
+ if (resultNode.type === 'suite') {
+ const suiteListNode = createDom(
+ 'ul',
+ { className: 'jasmine-suite', id: 'suite-' + resultNode.result.id },
+ createDom(
+ 'li',
+ {
+ className:
+ 'jasmine-suite-detail jasmine-' + resultNode.result.status
+ },
+ createDom(
+ 'a',
+ { href: specHref(resultNode.result) },
+ resultNode.result.description
+ )
+ )
+ );
+
+ summaryList(resultNode, suiteListNode);
+ domParent.appendChild(suiteListNode);
+ }
+ if (resultNode.type === 'spec') {
+ if (domParent.getAttribute('class') !== 'jasmine-specs') {
+ specListNode = createDom('ul', { className: 'jasmine-specs' });
+ domParent.appendChild(specListNode);
+ }
+ let specDescription = resultNode.result.description;
+ if (noExpectations(resultNode.result)) {
+ specDescription = 'SPEC HAS NO EXPECTATIONS ' + specDescription;
+ }
+ if (resultNode.result.status === 'pending') {
+ if (resultNode.result.pendingReason !== '') {
+ specDescription +=
+ ' PENDING WITH MESSAGE: ' + resultNode.result.pendingReason;
+ } else {
+ specDescription += ' PENDING';
+ }
+ }
+ specListNode.appendChild(
+ createDom(
+ 'li',
+ {
+ className: 'jasmine-' + resultNode.result.status,
+ id: 'spec-' + resultNode.result.id
+ },
+ createDom(
+ 'a',
+ { href: specHref(resultNode.result) },
+ specDescription
+ )
+ )
+ );
+ }
+ }
+ }
+
+ function optionsMenu(config) {
+ const optionsMenuDom = createDom(
+ 'div',
+ { className: 'jasmine-run-options' },
+ createDom('span', { className: 'jasmine-trigger' }, 'Options'),
+ createDom(
+ 'div',
+ { className: 'jasmine-payload' },
+ createDom(
+ 'div',
+ { className: 'jasmine-stop-on-failure' },
+ createDom('input', {
+ className: 'jasmine-fail-fast',
+ id: 'jasmine-fail-fast',
+ type: 'checkbox'
+ }),
+ createDom(
+ 'label',
+ { className: 'jasmine-label', for: 'jasmine-fail-fast' },
+ 'stop execution on spec failure'
+ )
+ ),
+ createDom(
+ 'div',
+ { className: 'jasmine-throw-failures' },
+ createDom('input', {
+ className: 'jasmine-throw',
+ id: 'jasmine-throw-failures',
+ type: 'checkbox'
+ }),
+ createDom(
+ 'label',
+ { className: 'jasmine-label', for: 'jasmine-throw-failures' },
+ 'stop spec on expectation failure'
+ )
+ ),
+ createDom(
+ 'div',
+ { className: 'jasmine-random-order' },
+ createDom('input', {
+ className: 'jasmine-random',
+ id: 'jasmine-random-order',
+ type: 'checkbox'
+ }),
+ createDom(
+ 'label',
+ { className: 'jasmine-label', for: 'jasmine-random-order' },
+ 'run tests in random order'
+ )
+ ),
+ createDom(
+ 'div',
+ { className: 'jasmine-hide-disabled' },
+ createDom('input', {
+ className: 'jasmine-disabled',
+ id: 'jasmine-hide-disabled',
+ type: 'checkbox'
+ }),
+ createDom(
+ 'label',
+ { className: 'jasmine-label', for: 'jasmine-hide-disabled' },
+ 'hide disabled tests'
+ )
+ )
+ )
+ );
+
+ const failFastCheckbox = optionsMenuDom.querySelector(
+ '#jasmine-fail-fast'
+ );
+ failFastCheckbox.checked = config.stopOnSpecFailure;
+ failFastCheckbox.onclick = function() {
+ navigateWithNewParam('stopOnSpecFailure', !config.stopOnSpecFailure);
+ };
+
+ const throwCheckbox = optionsMenuDom.querySelector(
+ '#jasmine-throw-failures'
+ );
+ throwCheckbox.checked = config.stopSpecOnExpectationFailure;
+ throwCheckbox.onclick = function() {
+ navigateWithNewParam(
+ 'stopSpecOnExpectationFailure',
+ !config.stopSpecOnExpectationFailure
+ );
+ };
+
+ const randomCheckbox = optionsMenuDom.querySelector(
+ '#jasmine-random-order'
+ );
+ randomCheckbox.checked = config.random;
+ randomCheckbox.onclick = function() {
+ navigateWithNewParam('random', !config.random);
+ };
+
+ const hideDisabled = optionsMenuDom.querySelector(
+ '#jasmine-hide-disabled'
+ );
+ hideDisabled.checked = config.hideDisabled;
+ hideDisabled.onclick = function() {
+ navigateWithNewParam('hideDisabled', !config.hideDisabled);
+ };
+
+ const optionsTrigger = optionsMenuDom.querySelector('.jasmine-trigger'),
+ optionsPayload = optionsMenuDom.querySelector('.jasmine-payload'),
+ isOpen = /\bjasmine-open\b/;
+
+ optionsTrigger.onclick = function() {
+ if (isOpen.test(optionsPayload.className)) {
+ optionsPayload.className = optionsPayload.className.replace(
+ isOpen,
+ ''
+ );
+ } else {
+ optionsPayload.className += ' jasmine-open';
+ }
+ };
+
+ return optionsMenuDom;
+ }
+
+ function failureDescription(result, suite) {
+ const wrapper = createDom(
+ 'div',
+ { className: 'jasmine-description' },
+ createDom(
+ 'a',
+ { title: result.description, href: specHref(result) },
+ result.description
+ )
+ );
+ let suiteLink;
+
+ while (suite && suite.parent) {
+ wrapper.insertBefore(createTextNode(' > '), wrapper.firstChild);
+ suiteLink = createDom(
+ 'a',
+ { href: suiteHref(suite) },
+ suite.result.description
+ );
+ wrapper.insertBefore(suiteLink, wrapper.firstChild);
+
+ suite = suite.parent;
+ }
+
+ return wrapper;
+ }
+
+ function suiteHref(suite) {
+ const els = [];
+
+ while (suite && suite.parent) {
+ els.unshift(suite.result.description);
+ suite = suite.parent;
+ }
+
+ // include window.location.pathname to fix issue with karma-jasmine-html-reporter in angular: see https://github.com/jasmine/jasmine/issues/1906
+ return (
+ (window.location.pathname || '') +
+ addToExistingQueryString('spec', els.join(' '))
+ );
+ }
+
+ function addDeprecationWarnings(result, runnableType) {
+ if (result && result.deprecationWarnings) {
+ for (let i = 0; i < result.deprecationWarnings.length; i++) {
+ const warning = result.deprecationWarnings[i].message;
+ deprecationWarnings.push({
+ message: warning,
+ stack: result.deprecationWarnings[i].stack,
+ runnableName: result.fullName,
+ runnableType: runnableType
+ });
+ }
+ }
+ }
+
+ function createExpander(stackTrace) {
+ const expandLink = createDom('a', { href: '#' }, 'Show stack trace');
+ const root = createDom(
+ 'div',
+ { className: 'jasmine-expander' },
+ expandLink,
+ createDom(
+ 'div',
+ { className: 'jasmine-expander-contents jasmine-stack-trace' },
+ stackTrace
+ )
+ );
+
+ expandLink.addEventListener('click', function(e) {
+ e.preventDefault();
+
+ if (root.classList.contains('jasmine-expanded')) {
+ root.classList.remove('jasmine-expanded');
+ expandLink.textContent = 'Show stack trace';
+ } else {
+ root.classList.add('jasmine-expanded');
+ expandLink.textContent = 'Hide stack trace';
+ }
+ });
+
+ return root;
+ }
+
+ function find(selector) {
+ return getContainer().querySelector('.jasmine_html-reporter ' + selector);
+ }
+
+ function clearPrior() {
+ const oldReporter = find('');
+
+ if (oldReporter) {
+ getContainer().removeChild(oldReporter);
+ }
+ }
+
+ function createDom(type, attrs, childrenArrayOrVarArgs) {
+ const el = createElement(type);
+ let children;
+
+ if (j$.isArray_(childrenArrayOrVarArgs)) {
+ children = childrenArrayOrVarArgs;
+ } else {
+ children = [];
+
+ for (let i = 2; i < arguments.length; i++) {
+ children.push(arguments[i]);
+ }
+ }
+
+ for (let i = 0; i < children.length; i++) {
+ const child = children[i];
+
+ if (typeof child === 'string') {
+ el.appendChild(createTextNode(child));
+ } else {
+ if (child) {
+ el.appendChild(child);
+ }
+ }
+ }
+
+ for (const attr in attrs) {
+ if (attr == 'className') {
+ el[attr] = attrs[attr];
+ } else {
+ el.setAttribute(attr, attrs[attr]);
+ }
+ }
+
+ return el;
+ }
+
+ function pluralize(singular, count) {
+ const word = count == 1 ? singular : singular + 's';
+
+ return '' + count + ' ' + word;
+ }
+
+ function specHref(result) {
+ // include window.location.pathname to fix issue with karma-jasmine-html-reporter in angular: see https://github.com/jasmine/jasmine/issues/1906
+ return (
+ (window.location.pathname || '') +
+ addToExistingQueryString('spec', result.fullName)
+ );
+ }
+
+ function seedHref(seed) {
+ // include window.location.pathname to fix issue with karma-jasmine-html-reporter in angular: see https://github.com/jasmine/jasmine/issues/1906
+ return (
+ (window.location.pathname || '') +
+ addToExistingQueryString('seed', seed)
+ );
+ }
+
+ function defaultQueryString(key, value) {
+ return '?' + key + '=' + value;
+ }
+
+ function setMenuModeTo(mode) {
+ htmlReporterMain.setAttribute('class', 'jasmine_html-reporter ' + mode);
+ }
+
+ function noExpectations(result) {
+ const allExpectations =
+ result.failedExpectations.length + result.passedExpectations.length;
+
+ return (
+ allExpectations === 0 &&
+ (result.status === 'passed' || result.status === 'failed')
+ );
+ }
+
+ function hasActiveSpec(resultNode) {
+ if (resultNode.type == 'spec' && resultNode.result.status != 'excluded') {
+ return true;
+ }
+
+ if (resultNode.type == 'suite') {
+ for (let i = 0, j = resultNode.children.length; i < j; i++) {
+ if (hasActiveSpec(resultNode.children[i])) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return HtmlReporter;
+};
+
+jasmineRequire.HtmlSpecFilter = function() {
+ function HtmlSpecFilter(options) {
+ const filterString =
+ options &&
+ options.filterString() &&
+ options.filterString().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
+ const filterPattern = new RegExp(filterString);
+
+ this.matches = function(specName) {
+ return filterPattern.test(specName);
+ };
+ }
+
+ return HtmlSpecFilter;
+};
+
+jasmineRequire.ResultsNode = function() {
+ function ResultsNode(result, type, parent) {
+ this.result = result;
+ this.type = type;
+ this.parent = parent;
+
+ this.children = [];
+
+ this.addChild = function(result, type) {
+ this.children.push(new ResultsNode(result, type, this));
+ };
+
+ this.last = function() {
+ return this.children[this.children.length - 1];
+ };
+
+ this.updateResult = function(result) {
+ this.result = result;
+ };
+ }
+
+ return ResultsNode;
+};
+
+jasmineRequire.QueryString = function() {
+ function QueryString(options) {
+ this.navigateWithNewParam = function(key, value) {
+ options.getWindowLocation().search = this.fullStringWithNewParam(
+ key,
+ value
+ );
+ };
+
+ this.fullStringWithNewParam = function(key, value) {
+ const paramMap = queryStringToParamMap();
+ paramMap[key] = value;
+ return toQueryString(paramMap);
+ };
+
+ this.getParam = function(key) {
+ return queryStringToParamMap()[key];
+ };
+
+ return this;
+
+ function toQueryString(paramMap) {
+ const qStrPairs = [];
+ for (const prop in paramMap) {
+ qStrPairs.push(
+ encodeURIComponent(prop) + '=' + encodeURIComponent(paramMap[prop])
+ );
+ }
+ return '?' + qStrPairs.join('&');
+ }
+
+ function queryStringToParamMap() {
+ const paramStr = options.getWindowLocation().search.substring(1);
+ let params = [];
+ const paramMap = {};
+
+ if (paramStr.length > 0) {
+ params = paramStr.split('&');
+ for (let i = 0; i < params.length; i++) {
+ const p = params[i].split('=');
+ let value = decodeURIComponent(p[1]);
+ if (value === 'true' || value === 'false') {
+ value = JSON.parse(value);
+ }
+ paramMap[decodeURIComponent(p[0])] = value;
+ }
+ }
+
+ return paramMap;
+ }
+ }
+
+ return QueryString;
+};
diff --git a/vendor/jasmine/jasmine.css b/vendor/jasmine/jasmine.css
new file mode 100644
index 000000000..1f364464b
--- /dev/null
+++ b/vendor/jasmine/jasmine.css
@@ -0,0 +1,298 @@
+@charset "UTF-8";
+body {
+ overflow-y: scroll;
+}
+
+.jasmine_html-reporter {
+ width: 100%;
+ background-color: #eee;
+ padding: 5px;
+ margin: -8px;
+ font-size: 11px;
+ font-family: Monaco, "Lucida Console", monospace;
+ line-height: 14px;
+ color: #333;
+}
+.jasmine_html-reporter a {
+ text-decoration: none;
+}
+.jasmine_html-reporter a:hover {
+ text-decoration: underline;
+}
+.jasmine_html-reporter p, .jasmine_html-reporter h1, .jasmine_html-reporter h2, .jasmine_html-reporter h3, .jasmine_html-reporter h4, .jasmine_html-reporter h5, .jasmine_html-reporter h6 {
+ margin: 0;
+ line-height: 14px;
+}
+.jasmine_html-reporter .jasmine-banner,
+.jasmine_html-reporter .jasmine-symbol-summary,
+.jasmine_html-reporter .jasmine-summary,
+.jasmine_html-reporter .jasmine-result-message,
+.jasmine_html-reporter .jasmine-spec .jasmine-description,
+.jasmine_html-reporter .jasmine-spec-detail .jasmine-description,
+.jasmine_html-reporter .jasmine-alert .jasmine-bar,
+.jasmine_html-reporter .jasmine-stack-trace {
+ padding-left: 9px;
+ padding-right: 9px;
+}
+.jasmine_html-reporter .jasmine-banner {
+ position: relative;
+}
+.jasmine_html-reporter .jasmine-banner .jasmine-title {
+ background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAAAZCAMAAACGusnyAAACdlBMVEX/////AP+AgICqVaqAQICZM5mAVYCSSZKAQICOOY6ATYCLRouAQICJO4mSSYCIRIiPQICHPIeOR4CGQ4aMQICGPYaLRoCFQ4WKQICPPYWJRYCOQoSJQICNPoSIRICMQoSHQICHRICKQoOHQICKPoOJO4OJQYOMQICMQ4CIQYKLQICIPoKLQ4CKQICNPoKJQISMQ4KJQoSLQYKJQISLQ4KIQoSKQYKIQICIQISMQoSKQYKLQIOLQoOJQYGLQIOKQIOMQoGKQYOLQYGKQIOLQoGJQYOJQIOKQYGJQIOKQoGKQIGLQIKLQ4KKQoGLQYKJQIGKQYKJQIGKQIKJQoGKQYKLQIGKQYKLQIOJQoKKQoOJQYKKQIOJQoKKQoOKQIOLQoKKQYOLQYKJQIOKQoKKQYKKQoKJQYOKQYKLQIOKQoKLQYOKQYKLQIOJQoGKQYKJQYGJQoGKQYKLQoGLQYGKQoGJQYKKQYGJQIKKQoGJQYKLQIKKQYGLQYKKQYGKQYGKQYKJQYOKQoKJQYOKQYKLQYOLQYOKQYKLQYOKQoKKQYKKQYOKQYOJQYKKQYKLQYKKQIKKQoKKQYKKQYKKQoKJQIKKQYKLQYKKQYKKQIKKQYKKQYKKQYKKQIKKQYKJQYGLQYGKQYKKQYKKQYGKQIKKQYGKQYOJQoKKQYOLQYKKQYOKQoKKQYKKQoKKQYKKQYKJQYKLQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKJQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKLQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKmIDpEAAAA0XRSTlMAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyAiIyQlJycoKissLS4wMTQ1Njc4OTo7PDw+P0BCQ0RISUpLTE1OUFNUVVdYWFlaW15fYGFiY2ZnaGlqa2xtb3BxcnN0dnh5ent8fX5/gIGChIWIioyNjo+QkZOUlZaYmZqbnJ2eoKGio6WmqKmsra6vsLGztre4ubq7vL2+wMHDxMjJysvNzs/Q0dLU1tfY2dvc3t/g4eLj5ebn6Onq6+zt7u/w8vP09fb3+Pn6+/z9/vkVQXAAAAMaSURBVHhe5dXxV1N1GMfxz2ABbDgIAm5VDJOyVDIJLUMaVpBWUZUaGbmqoGpZRSiGiRWp6KoZ5AB0ZY50RImZQIlahKkMYXv/R90dBvET/rJfOr3Ouc8v99zPec59zvf56j+vYKlViSf7250X4Mr3O29Tgq08BdGB4DhcekEJ5YkQKFsgWZdtj9JpV+I8xPjLFqkrsEIqO8PHSpis36jWazcqjEsfJjkvRssVU37SdIOu4XCf5vEJPsnwJpnRNU9JmxhMk8l1gehIrq7hTFjzOD+Vf88629qKMJVNltInFeRexRQyJlNeqd1iGDlSzrIUIyXbyFfm3RYprcQRe7lqtWyGYbfc6dT0R2vmdOOkX3u55C1rP37ftiH+tDby4r/RBT0w8TyEkr+epB9XgPDmSYYWbrhCuFYaIyw3fDQAXTnSkh+ANofiHmWf9l+FY1I90FdQTetstO00o23novzVsJ7uB3/C5TkbjRwZ5JerwV4iRWq9HFbFMaK/d0TYqayRiQPuIxxS3Bu8JWU90/60tKi7vkhaznez0a/TbVOKj5CaOZh6fWG6/Lyv9B/ZLR1gw/S/fpbeVD3MCW1li6SvWDOn65tr99/uvWtBS0XDm4s1t+sOHpG0kpBKx/l77wOSnxLpcx6TXmXLTPQOKYOf9Q1dfr8/SJ2mFdCvl1Yl93DiHUZvXeLJbGSzYu5gVJ2slbSakOR8dxCq5adQ2oFLqsE9Ex3L4qQO0eOPeU5x56bypXp4onSEb5OkICX6lDat55TeoztNKQcJaakrz9KCb95oD69IKq+yKW4XPjknaS52V0TZqE2cTtXjcHSCRmUO88e+85hj3EP74i9p8pylw7lxgMDyyl6OV7ZejnjNMfatu87LxRbH0IS35gt2a4ZjmGpVBdKK3Wr6INk8jWWSGqbA55CKgjBRC6E9w78ydTg3ABS3AFV1QN0Y4Aa2pgEjWnQURj9L0ayK6R2ysEqxHUKzYnLvvyU+i9KM2JHJzE4vyZOyDcOwOsySajeLPc8sNvPJkFlyJd20wpqAzZeAfZ3oWybxd+P/3j+SG3uSBdf2VQAAAABJRU5ErkJggg==") no-repeat;
+ background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiCiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgdmVyc2lvbj0iMS4xIgogICB3aWR0aD0iNjgxLjk2MjUyIgogICBoZWlnaHQ9IjE4Ny41IgogICBpZD0ic3ZnMiIKICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PG1ldGFkYXRhCiAgICAgaWQ9Im1ldGFkYXRhOCI+PHJkZjpSREY+PGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PjxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz48L2NjOldvcms+PC9yZGY6UkRGPjwvbWV0YWRhdGE+PGRlZnMKICAgICBpZD0iZGVmczYiPjxjbGlwUGF0aAogICAgICAgaWQ9ImNsaXBQYXRoMTgiPjxwYXRoCiAgICAgICAgIGQ9Ik0gMCwxNTAwIDAsMCBsIDU0NTUuNzQsMCAwLDE1MDAgTCAwLDE1MDAgeiIKICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgaWQ9InBhdGgyMCIgLz48L2NsaXBQYXRoPjwvZGVmcz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMjUsMCwwLC0xLjI1LDAsMTg3LjUpIgogICAgIGlkPSJnMTAiPjxnCiAgICAgICB0cmFuc2Zvcm09InNjYWxlKDAuMSwwLjEpIgogICAgICAgaWQ9ImcxMiI+PGcKICAgICAgICAgaWQ9ImcxNCI+PGcKICAgICAgICAgICBjbGlwLXBhdGg9InVybCgjY2xpcFBhdGgxOCkiCiAgICAgICAgICAgaWQ9ImcxNiI+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gMTU0NCw1OTkuNDM0IGMgMC45MiwtNDAuMzUyIDI1LjY4LC04MS42MDIgNzEuNTMsLTgxLjYwMiAyNy41MSwwIDQ3LjY4LDEyLjgzMiA2MS40NCwzNS43NTQgMTIuODMsMjIuOTMgMTIuODMsNTYuODUyIDEyLjgzLDgyLjUyNyBsIDAsMzI5LjE4NCAtNzEuNTIsMCAwLDEwNC41NDMgMjY2LjgzLDAgMCwtMTA0LjU0MyAtNzAuNiwwIDAsLTM0NC43NyBjIDAsLTU4LjY5MSAtMy42OCwtMTA0LjUzMSAtNDQuOTMsLTE1Mi4yMTggLTM2LjY4LC00Mi4xOCAtOTYuMjgsLTY2LjAyIC0xNTMuMTQsLTY2LjAyIC0xMTcuMzcsMCAtMjA3LjI0LDc3Ljk0MSAtMjAyLjY0LDE5Ny4xNDUgbCAxMzAuMiwwIgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMjIiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDIzMDEuNCw2NjIuNjk1IGMgMCw4MC43MDMgLTY2Ljk0LDE0NS44MTMgLTE0Ny42MywxNDUuODEzIC04My40NCwwIC0xNDcuNjMsLTY4Ljc4MSAtMTQ3LjYzLC0xNTEuMzAxIDAsLTc5Ljc4NSA2Ni45NCwtMTQ1LjgwMSAxNDUuOCwtMTQ1LjgwMSA4NC4zNSwwIDE0OS40Niw2Ny44NTIgMTQ5LjQ2LDE1MS4yODkgeiBtIC0xLjgzLC0xODEuNTQ3IGMgLTM1Ljc3LC01NC4wOTcgLTkzLjUzLC03OC44NTkgLTE1Ny43MiwtNzguODU5IC0xNDAuMywwIC0yNTEuMjQsMTE2LjQ0OSAtMjUxLjI0LDI1NC45MTggMCwxNDIuMTI5IDExMy43LDI2MC40MSAyNTYuNzQsMjYwLjQxIDYzLjI3LDAgMTE4LjI5LC0yOS4zMzYgMTUyLjIyLC04Mi41MjMgbCAwLDY5LjY4NyAxNzUuMTQsMCAwLC0xMDQuNTI3IC02MS40NCwwIDAsLTI4MC41OTggNjEuNDQsMCAwLC0xMDQuNTI3IC0xNzUuMTQsMCAwLDY2LjAxOSIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSAyNjIyLjMzLDU1Ny4yNTggYyAzLjY3LC00NC4wMTYgMzMuMDEsLTczLjM0OCA3OC44NiwtNzMuMzQ4IDMzLjkzLDAgNjYuOTMsMjMuODI0IDY2LjkzLDYwLjUwNCAwLDQ4LjYwNiAtNDUuODQsNTYuODU2IC04My40NCw2Ni45NDEgLTg1LjI4LDIyLjAwNCAtMTc4LjgxLDQ4LjYwNiAtMTc4LjgxLDE1NS44NzkgMCw5My41MzYgNzguODYsMTQ3LjYzMyAxNjUuOTgsMTQ3LjYzMyA0NCwwIDgzLjQzLC05LjE3NiAxMTAuOTQsLTQ0LjAwOCBsIDAsMzMuOTIyIDgyLjUzLDAgMCwtMTMyLjk2NSAtMTA4LjIxLDAgYyAtMS44MywzNC44NTYgLTI4LjQyLDU3Ljc3NCAtNjMuMjYsNTcuNzc0IC0zMC4yNiwwIC02Mi4zNSwtMTcuNDIyIC02Mi4zNSwtNTEuMzQ4IDAsLTQ1Ljg0NyA0NC45MywtNTUuOTMgODAuNjksLTY0LjE4IDg4LjAyLC0yMC4xNzUgMTgyLjQ3LC00Ny42OTUgMTgyLjQ3LC0xNTcuNzM0IDAsLTk5LjAyNyAtODMuNDQsLTE1NC4wMzkgLTE3NS4xMywtMTU0LjAzOSAtNDkuNTMsMCAtOTQuNDYsMTUuNTgyIC0xMjYuNTUsNTMuMTggbCAwLC00MC4zNCAtODUuMjcsMCAwLDE0Mi4xMjkgMTE0LjYyLDAiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgaWQ9InBhdGgyNiIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiM4YTQxODI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiIC8+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gMjk4OC4xOCw4MDAuMjU0IC02My4yNiwwIDAsMTA0LjUyNyAxNjUuMDUsMCAwLC03My4zNTUgYyAzMS4xOCw1MS4zNDcgNzguODYsODUuMjc3IDE0MS4yMSw4NS4yNzcgNjcuODUsMCAxMjQuNzEsLTQxLjI1OCAxNTIuMjEsLTEwMi42OTkgMjYuNiw2Mi4zNTEgOTIuNjIsMTAyLjY5OSAxNjAuNDcsMTAyLjY5OSA1My4xOSwwIDEwNS40NiwtMjIgMTQxLjIxLC02Mi4zNTEgMzguNTIsLTQ0LjkzOCAzOC41MiwtOTMuNTMyIDM4LjUyLC0xNDkuNDU3IGwgMCwtMTg1LjIzOSA2My4yNywwIDAsLTEwNC41MjcgLTIzOC40MiwwIDAsMTA0LjUyNyA2My4yOCwwIDAsMTU3LjcxNSBjIDAsMzIuMTAyIDAsNjAuNTI3IC0xNC42Nyw4OC45NTcgLTE4LjM0LDI2LjU4MiAtNDguNjEsNDAuMzQ0IC03OS43Nyw0MC4zNDQgLTMwLjI2LDAgLTYzLjI4LC0xMi44NDQgLTgyLjUzLC0zNi42NzIgLTIyLjkzLC0yOS4zNTUgLTIyLjkzLC01Ni44NjMgLTIyLjkzLC05Mi42MjkgbCAwLC0xNTcuNzE1IDYzLjI3LDAgMCwtMTA0LjUyNyAtMjM4LjQxLDAgMCwxMDQuNTI3IDYzLjI4LDAgMCwxNTAuMzgzIGMgMCwyOS4zNDggMCw2Ni4wMjMgLTE0LjY3LDkxLjY5OSAtMTUuNTksMjkuMzM2IC00Ny42OSw0NC45MzQgLTgwLjcsNDQuOTM0IC0zMS4xOCwwIC01Ny43NywtMTEuMDA4IC03Ny45NCwtMzUuNzc0IC0yNC43NywtMzAuMjUzIC0yNi42LC02Mi4zNDMgLTI2LjYsLTk5Ljk0MSBsIDAsLTE1MS4zMDEgNjMuMjcsMCAwLC0xMDQuNTI3IC0yMzguNCwwIDAsMTA0LjUyNyA2My4yNiwwIDAsMjgwLjU5OCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDI4IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSAzOTk4LjY2LDk1MS41NDcgLTExMS44NywwIDAsMTE4LjI5MyAxMTEuODcsMCAwLC0xMTguMjkzIHogbSAwLC00MzEuODkxIDYzLjI3LDAgMCwtMTA0LjUyNyAtMjM5LjMzLDAgMCwxMDQuNTI3IDY0LjE5LDAgMCwyODAuNTk4IC02My4yNywwIDAsMTA0LjUyNyAxNzUuMTQsMCAwLC0zODUuMTI1IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMzAiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDQxNTkuMTIsODAwLjI1NCAtNjMuMjcsMCAwLDEwNC41MjcgMTc1LjE0LDAgMCwtNjkuNjg3IGMgMjkuMzUsNTQuMTAxIDg0LjM2LDgwLjY5OSAxNDQuODcsODAuNjk5IDUzLjE5LDAgMTA1LjQ1LC0yMi4wMTYgMTQxLjIyLC02MC41MjcgNDAuMzQsLTQ0LjkzNCA0MS4yNiwtODguMDMyIDQxLjI2LC0xNDMuOTU3IGwgMCwtMTkxLjY1MyA2My4yNywwIDAsLTEwNC41MjcgLTIzOC40LDAgMCwxMDQuNTI3IDYzLjI2LDAgMCwxNTguNjM3IGMgMCwzMC4yNjIgMCw2MS40MzQgLTE5LjI2LDg4LjAzNSAtMjAuMTcsMjYuNTgyIC01My4xOCwzOS40MTQgLTg2LjE5LDM5LjQxNCAtMzMuOTMsMCAtNjguNzcsLTEzLjc1IC04OC45NCwtNDEuMjUgLTIxLjA5LC0yNy41IC0yMS4wOSwtNjkuNjg3IC0yMS4wOSwtMTAyLjcwNyBsIDAsLTE0Mi4xMjkgNjMuMjYsMCAwLC0xMDQuNTI3IC0yMzguNCwwIDAsMTA0LjUyNyA2My4yNywwIDAsMjgwLjU5OCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDMyIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA1MDgyLjQ4LDcwMy45NjUgYyAtMTkuMjQsNzAuNjA1IC04MS42LDExNS41NDcgLTE1NC4wNCwxMTUuNTQ3IC02Ni4wNCwwIC0xMjkuMywtNTEuMzQ4IC0xNDMuMDUsLTExNS41NDcgbCAyOTcuMDksMCB6IG0gODUuMjcsLTE0NC44ODMgYyAtMzguNTEsLTkzLjUyMyAtMTI5LjI3LC0xNTYuNzkzIC0yMzEuMDUsLTE1Ni43OTMgLTE0My4wNywwIC0yNTcuNjgsMTExLjg3MSAtMjU3LjY4LDI1NS44MzYgMCwxNDQuODgzIDEwOS4xMiwyNjEuMzI4IDI1NC45MSwyNjEuMzI4IDY3Ljg3LDAgMTM1LjcyLC0zMC4yNTggMTgzLjM5LC03OC44NjMgNDguNjIsLTUxLjM0NCA2OC43OSwtMTEzLjY5NSA2OC43OSwtMTgzLjM4MyBsIC0zLjY3LC0zOS40MzQgLTM5Ni4xMywwIGMgMTQuNjcsLTY3Ljg2MyA3Ny4wMywtMTE3LjM2MyAxNDYuNzIsLTExNy4zNjMgNDguNTksMCA5MC43NiwxOC4zMjggMTE4LjI4LDU4LjY3MiBsIDExNi40NCwwIgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMzQiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDY5MC44OTUsODUwLjcwMyA5MC43NSwwIDIyLjU0MywzMS4wMzUgMCwyNDMuMTIyIC0xMzUuODI5LDAgMCwtMjQzLjE0MSAyMi41MzYsLTMxLjAxNiIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDM2IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA2MzIuMzk1LDc0Mi4yNTggMjguMDM5LDg2LjMwNCAtMjIuNTUxLDMxLjA0IC0yMzEuMjIzLDc1LjEyOCAtNDEuOTc2LC0xMjkuMTgzIDIzMS4yNTcsLTc1LjEzNyAzNi40NTQsMTEuODQ4IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMzgiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDcxNy40NDksNjUzLjEwNSAtNzMuNDEsNTMuMzYgLTM2LjQ4OCwtMTEuODc1IC0xNDIuOTAzLC0xOTYuNjkyIDEwOS44ODMsLTc5LjgyOCAxNDIuOTE4LDE5Ni43MDMgMCwzOC4zMzIiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgaWQ9InBhdGg0MCIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiM4YTQxODI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiIC8+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gODI4LjUyLDcwNi40NjUgLTczLjQyNiwtNTMuMzQgMC4wMTEsLTM4LjM1OSBMIDg5OC4wMDQsNDE4LjA3IDEwMDcuOSw0OTcuODk4IDg2NC45NzMsNjk0LjYwOSA4MjguNTIsNzA2LjQ2NSIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDQyIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA4MTIuMDg2LDgyOC41ODYgMjguMDU1LC04Ni4zMiAzNi40ODQsLTExLjgzNiAyMzEuMjI1LDc1LjExNyAtNDEuOTcsMTI5LjE4MyAtMjMxLjIzOSwtNzUuMTQgLTIyLjU1NSwtMzEuMDA0IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoNDQiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDczNi4zMDEsMTMzNS44OCBjIC0zMjMuMDQ3LDAgLTU4NS44NzUsLTI2Mi43OCAtNTg1Ljg3NSwtNTg1Ljc4MiAwLC0zMjMuMTE4IDI2Mi44MjgsLTU4NS45NzcgNTg1Ljg3NSwtNTg1Ljk3NyAzMjMuMDE5LDAgNTg1LjgwOSwyNjIuODU5IDU4NS44MDksNTg1Ljk3NyAwLDMyMy4wMDIgLTI2Mi43OSw1ODUuNzgyIC01ODUuODA5LDU4NS43ODIgbCAwLDAgeiBtIDAsLTExOC42MSBjIDI1Ny45NzIsMCA0NjcuMTg5LC0yMDkuMTMgNDY3LjE4OSwtNDY3LjE3MiAwLC0yNTguMTI5IC0yMDkuMjE3LC00NjcuMzQ4IC00NjcuMTg5LC00NjcuMzQ4IC0yNTguMDc0LDAgLTQ2Ny4yNTQsMjA5LjIxOSAtNDY3LjI1NCw0NjcuMzQ4IDAsMjU4LjA0MiAyMDkuMTgsNDY3LjE3MiA0NjcuMjU0LDQ2Ny4xNzIiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgaWQ9InBhdGg0NiIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiM4YTQxODI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiIC8+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gMTA5MS4xMyw2MTkuODgzIC0xNzUuNzcxLDU3LjEyMSAxMS42MjksMzUuODA4IDE3NS43NjIsLTU3LjEyMSAtMTEuNjIsLTM1LjgwOCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDQ4IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0iTSA4NjYuOTU3LDkwMi4wNzQgODM2LjUsOTI0LjE5OSA5NDUuMTIxLDEwNzMuNzMgOTc1LjU4NiwxMDUxLjYxIDg2Ni45NTcsOTAyLjA3NCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDUwIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0iTSA2MDcuNDY1LDkwMy40NDUgNDk4Ljg1NSwxMDUyLjk3IDUyOS4zMiwxMDc1LjEgNjM3LjkzLDkyNS41NjYgNjA3LjQ2NSw5MDMuNDQ1IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoNTIiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDM4MC42ODgsNjIyLjEyOSAtMTEuNjI2LDM1LjgwMSAxNzUuNzU4LDU3LjA5IDExLjYyMSwtMzUuODAxIC0xNzUuNzUzLC01Ny4wOSIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDU0IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA3MTYuMjg5LDM3Ni41OSAzNy42NDA2LDAgMCwxODQuODE2IC0zNy42NDA2LDAgMCwtMTg0LjgxNiB6IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoNTYiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjwvZz48L2c+PC9nPjwvZz48L3N2Zz4=") no-repeat, none;
+ background-size: 100%;
+ display: block;
+ float: left;
+ width: 90px;
+ height: 25px;
+}
+.jasmine_html-reporter .jasmine-banner .jasmine-version {
+ margin-left: 14px;
+ position: relative;
+ top: 6px;
+}
+.jasmine_html-reporter #jasmine_content {
+ position: fixed;
+ right: 100%;
+}
+.jasmine_html-reporter .jasmine-banner {
+ margin-top: 14px;
+}
+.jasmine_html-reporter .jasmine-duration {
+ color: #fff;
+ float: right;
+ line-height: 28px;
+ padding-right: 9px;
+}
+.jasmine_html-reporter .jasmine-symbol-summary {
+ overflow: hidden;
+ margin: 14px 0;
+}
+.jasmine_html-reporter .jasmine-symbol-summary li {
+ display: inline-block;
+ height: 10px;
+ width: 14px;
+ font-size: 16px;
+}
+.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-passed {
+ font-size: 14px;
+}
+.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-passed:before {
+ color: #007069;
+ content: "•";
+}
+.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-failed {
+ line-height: 9px;
+}
+.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-failed:before {
+ color: #ca3a11;
+ content: "×";
+ font-weight: bold;
+ margin-left: -1px;
+}
+.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-excluded {
+ font-size: 14px;
+}
+.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-excluded:before {
+ color: #bababa;
+ content: "•";
+}
+.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-excluded-no-display {
+ font-size: 14px;
+ display: none;
+}
+.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-pending {
+ line-height: 17px;
+}
+.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-pending:before {
+ color: #ba9d37;
+ content: "*";
+}
+.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-empty {
+ font-size: 14px;
+}
+.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-empty:before {
+ color: #ba9d37;
+ content: "•";
+}
+.jasmine_html-reporter .jasmine-run-options {
+ float: right;
+ margin-right: 5px;
+ border: 1px solid #8a4182;
+ color: #8a4182;
+ position: relative;
+ line-height: 20px;
+}
+.jasmine_html-reporter .jasmine-run-options .jasmine-trigger {
+ cursor: pointer;
+ padding: 8px 16px;
+}
+.jasmine_html-reporter .jasmine-run-options .jasmine-payload {
+ position: absolute;
+ display: none;
+ right: -1px;
+ border: 1px solid #8a4182;
+ background-color: #eee;
+ white-space: nowrap;
+ padding: 4px 8px;
+}
+.jasmine_html-reporter .jasmine-run-options .jasmine-payload.jasmine-open {
+ display: block;
+}
+.jasmine_html-reporter .jasmine-bar {
+ line-height: 28px;
+ font-size: 14px;
+ display: block;
+ color: #eee;
+}
+.jasmine_html-reporter .jasmine-bar.jasmine-failed, .jasmine_html-reporter .jasmine-bar.jasmine-errored {
+ background-color: #ca3a11;
+ border-bottom: 1px solid #eee;
+}
+.jasmine_html-reporter .jasmine-bar.jasmine-passed {
+ background-color: #007069;
+}
+.jasmine_html-reporter .jasmine-bar.jasmine-incomplete {
+ background-color: #bababa;
+}
+.jasmine_html-reporter .jasmine-bar.jasmine-skipped {
+ background-color: #bababa;
+}
+.jasmine_html-reporter .jasmine-bar.jasmine-warning {
+ margin-top: 14px;
+ margin-bottom: 14px;
+ background-color: #ba9d37;
+ color: #333;
+}
+.jasmine_html-reporter .jasmine-bar.jasmine-menu {
+ background-color: #fff;
+ color: #000;
+}
+.jasmine_html-reporter .jasmine-bar.jasmine-menu a {
+ color: blue;
+ text-decoration: underline;
+}
+.jasmine_html-reporter .jasmine-bar a {
+ color: white;
+}
+.jasmine_html-reporter.jasmine-spec-list .jasmine-bar.jasmine-menu.jasmine-failure-list,
+.jasmine_html-reporter.jasmine-spec-list .jasmine-results .jasmine-failures {
+ display: none;
+}
+.jasmine_html-reporter.jasmine-failure-list .jasmine-bar.jasmine-menu.jasmine-spec-list,
+.jasmine_html-reporter.jasmine-failure-list .jasmine-summary {
+ display: none;
+}
+.jasmine_html-reporter .jasmine-results {
+ margin-top: 14px;
+}
+.jasmine_html-reporter .jasmine-summary {
+ margin-top: 14px;
+}
+.jasmine_html-reporter .jasmine-summary ul {
+ list-style-type: none;
+ margin-left: 14px;
+ padding-top: 0;
+ padding-left: 0;
+}
+.jasmine_html-reporter .jasmine-summary ul.jasmine-suite {
+ margin-top: 7px;
+ margin-bottom: 7px;
+}
+.jasmine_html-reporter .jasmine-summary li.jasmine-passed a {
+ color: #007069;
+}
+.jasmine_html-reporter .jasmine-summary li.jasmine-failed a {
+ color: #ca3a11;
+}
+.jasmine_html-reporter .jasmine-summary li.jasmine-empty a {
+ color: #ba9d37;
+}
+.jasmine_html-reporter .jasmine-summary li.jasmine-pending a {
+ color: #ba9d37;
+}
+.jasmine_html-reporter .jasmine-summary li.jasmine-excluded a {
+ color: #bababa;
+}
+.jasmine_html-reporter .jasmine-specs li.jasmine-passed a:before {
+ content: "• ";
+}
+.jasmine_html-reporter .jasmine-specs li.jasmine-failed a:before {
+ content: "× ";
+}
+.jasmine_html-reporter .jasmine-specs li.jasmine-empty a:before {
+ content: "* ";
+}
+.jasmine_html-reporter .jasmine-specs li.jasmine-pending a:before {
+ content: "• ";
+}
+.jasmine_html-reporter .jasmine-specs li.jasmine-excluded a:before {
+ content: "• ";
+}
+.jasmine_html-reporter .jasmine-description + .jasmine-suite {
+ margin-top: 0;
+}
+.jasmine_html-reporter .jasmine-suite {
+ margin-top: 14px;
+}
+.jasmine_html-reporter .jasmine-suite a {
+ color: #333;
+}
+.jasmine_html-reporter .jasmine-failures .jasmine-spec-detail {
+ margin-bottom: 28px;
+}
+.jasmine_html-reporter .jasmine-failures .jasmine-spec-detail .jasmine-description {
+ background-color: #ca3a11;
+ color: white;
+}
+.jasmine_html-reporter .jasmine-failures .jasmine-spec-detail .jasmine-description a {
+ color: white;
+}
+.jasmine_html-reporter .jasmine-result-message {
+ padding-top: 14px;
+ color: #333;
+ white-space: pre-wrap;
+}
+.jasmine_html-reporter .jasmine-result-message span.jasmine-result {
+ display: block;
+}
+.jasmine_html-reporter .jasmine-stack-trace {
+ margin: 5px 0 0 0;
+ max-height: 224px;
+ overflow: auto;
+ line-height: 18px;
+ color: #666;
+ border: 1px solid #ddd;
+ background: white;
+ white-space: pre;
+}
+.jasmine_html-reporter .jasmine-expander a {
+ display: block;
+ margin-left: 14px;
+ color: blue;
+ text-decoration: underline;
+}
+.jasmine_html-reporter .jasmine-expander-contents {
+ display: none;
+}
+.jasmine_html-reporter .jasmine-expanded {
+ padding-bottom: 10px;
+}
+.jasmine_html-reporter .jasmine-expanded .jasmine-expander-contents {
+ display: block;
+ margin-left: 14px;
+ padding: 5px;
+}
+.jasmine_html-reporter .jasmine-debug-log {
+ margin: 5px 0 0 0;
+ padding: 5px;
+ color: #666;
+ border: 1px solid #ddd;
+ background: white;
+}
+.jasmine_html-reporter .jasmine-debug-log table {
+ border-spacing: 0;
+}
+.jasmine_html-reporter .jasmine-debug-log table, .jasmine_html-reporter .jasmine-debug-log th, .jasmine_html-reporter .jasmine-debug-log td {
+ border: 1px solid #ddd;
+}
\ No newline at end of file
diff --git a/vendor/jasmine/jasmine.js b/vendor/jasmine/jasmine.js
new file mode 100644
index 000000000..79a0c317d
--- /dev/null
+++ b/vendor/jasmine/jasmine.js
@@ -0,0 +1,10816 @@
+/*
+Copyright (c) 2008-2023 Pivotal Labs
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+// eslint-disable-next-line no-unused-vars,no-var
+var getJasmineRequireObj = (function(jasmineGlobal) {
+ let jasmineRequire;
+
+ if (
+ typeof module !== 'undefined' &&
+ module.exports &&
+ typeof exports !== 'undefined'
+ ) {
+ if (typeof global !== 'undefined') {
+ jasmineGlobal = global;
+ } else {
+ jasmineGlobal = {};
+ }
+ jasmineRequire = exports;
+ } else {
+ if (
+ typeof window !== 'undefined' &&
+ typeof window.toString === 'function' &&
+ window.toString() === '[object GjsGlobal]'
+ ) {
+ jasmineGlobal = window;
+ }
+ jasmineRequire = jasmineGlobal.jasmineRequire = {};
+ }
+
+ function getJasmineRequire() {
+ return jasmineRequire;
+ }
+
+ getJasmineRequire().core = function(jRequire) {
+ const j$ = {};
+
+ jRequire.base(j$, jasmineGlobal);
+ j$.util = jRequire.util(j$);
+ j$.errors = jRequire.errors();
+ j$.formatErrorMsg = jRequire.formatErrorMsg();
+ j$.Any = jRequire.Any(j$);
+ j$.Anything = jRequire.Anything(j$);
+ j$.CallTracker = jRequire.CallTracker(j$);
+ j$.MockDate = jRequire.MockDate(j$);
+ j$.getClearStack = jRequire.clearStack(j$);
+ j$.Clock = jRequire.Clock();
+ j$.DelayedFunctionScheduler = jRequire.DelayedFunctionScheduler(j$);
+ j$.Deprecator = jRequire.Deprecator(j$);
+ j$.Env = jRequire.Env(j$);
+ j$.StackTrace = jRequire.StackTrace(j$);
+ j$.ExceptionFormatter = jRequire.ExceptionFormatter(j$);
+ j$.ExpectationFilterChain = jRequire.ExpectationFilterChain();
+ j$.Expector = jRequire.Expector(j$);
+ j$.Expectation = jRequire.Expectation(j$);
+ j$.buildExpectationResult = jRequire.buildExpectationResult(j$);
+ j$.JsApiReporter = jRequire.JsApiReporter(j$);
+ j$.makePrettyPrinter = jRequire.makePrettyPrinter(j$);
+ j$.basicPrettyPrinter_ = j$.makePrettyPrinter();
+ j$.MatchersUtil = jRequire.MatchersUtil(j$);
+ j$.ObjectContaining = jRequire.ObjectContaining(j$);
+ j$.ArrayContaining = jRequire.ArrayContaining(j$);
+ j$.ArrayWithExactContents = jRequire.ArrayWithExactContents(j$);
+ j$.MapContaining = jRequire.MapContaining(j$);
+ j$.SetContaining = jRequire.SetContaining(j$);
+ j$.QueueRunner = jRequire.QueueRunner(j$);
+ j$.NeverSkipPolicy = jRequire.NeverSkipPolicy(j$);
+ j$.SkipAfterBeforeAllErrorPolicy = jRequire.SkipAfterBeforeAllErrorPolicy(
+ j$
+ );
+ j$.CompleteOnFirstErrorSkipPolicy = jRequire.CompleteOnFirstErrorSkipPolicy(
+ j$
+ );
+ j$.reporterEvents = jRequire.reporterEvents(j$);
+ j$.ReportDispatcher = jRequire.ReportDispatcher(j$);
+ j$.ParallelReportDispatcher = jRequire.ParallelReportDispatcher(j$);
+ j$.RunableResources = jRequire.RunableResources(j$);
+ j$.Runner = jRequire.Runner(j$);
+ j$.Spec = jRequire.Spec(j$);
+ j$.Spy = jRequire.Spy(j$);
+ j$.SpyFactory = jRequire.SpyFactory(j$);
+ j$.SpyRegistry = jRequire.SpyRegistry(j$);
+ j$.SpyStrategy = jRequire.SpyStrategy(j$);
+ j$.StringMatching = jRequire.StringMatching(j$);
+ j$.StringContaining = jRequire.StringContaining(j$);
+ j$.UserContext = jRequire.UserContext(j$);
+ j$.Suite = jRequire.Suite(j$);
+ j$.SuiteBuilder = jRequire.SuiteBuilder(j$);
+ j$.Timer = jRequire.Timer();
+ j$.TreeProcessor = jRequire.TreeProcessor();
+ j$.version = jRequire.version();
+ j$.Order = jRequire.Order();
+ j$.DiffBuilder = jRequire.DiffBuilder(j$);
+ j$.NullDiffBuilder = jRequire.NullDiffBuilder(j$);
+ j$.ObjectPath = jRequire.ObjectPath(j$);
+ j$.MismatchTree = jRequire.MismatchTree(j$);
+ j$.GlobalErrors = jRequire.GlobalErrors(j$);
+
+ j$.Truthy = jRequire.Truthy(j$);
+ j$.Falsy = jRequire.Falsy(j$);
+ j$.Empty = jRequire.Empty(j$);
+ j$.NotEmpty = jRequire.NotEmpty(j$);
+ j$.Is = jRequire.Is(j$);
+
+ j$.matchers = jRequire.requireMatchers(jRequire, j$);
+ j$.asyncMatchers = jRequire.requireAsyncMatchers(jRequire, j$);
+
+ return j$;
+ };
+
+ return getJasmineRequire;
+})(this);
+
+getJasmineRequireObj().requireMatchers = function(jRequire, j$) {
+ const availableMatchers = [
+ 'nothing',
+ 'toBe',
+ 'toBeCloseTo',
+ 'toBeDefined',
+ 'toBeInstanceOf',
+ 'toBeFalse',
+ 'toBeFalsy',
+ 'toBeGreaterThan',
+ 'toBeGreaterThanOrEqual',
+ 'toBeLessThan',
+ 'toBeLessThanOrEqual',
+ 'toBeNaN',
+ 'toBeNegativeInfinity',
+ 'toBeNull',
+ 'toBePositiveInfinity',
+ 'toBeTrue',
+ 'toBeTruthy',
+ 'toBeUndefined',
+ 'toContain',
+ 'toEqual',
+ 'toHaveSize',
+ 'toHaveBeenCalled',
+ 'toHaveBeenCalledBefore',
+ 'toHaveBeenCalledOnceWith',
+ 'toHaveBeenCalledTimes',
+ 'toHaveBeenCalledWith',
+ 'toHaveClass',
+ 'toHaveSpyInteractions',
+ 'toMatch',
+ 'toThrow',
+ 'toThrowError',
+ 'toThrowMatching'
+ ],
+ matchers = {};
+
+ for (const name of availableMatchers) {
+ matchers[name] = jRequire[name](j$);
+ }
+
+ return matchers;
+};
+
+getJasmineRequireObj().base = function(j$, jasmineGlobal) {
+ /**
+ * Maximum object depth the pretty printer will print to.
+ * Set this to a lower value to speed up pretty printing if you have large objects.
+ * @name jasmine.MAX_PRETTY_PRINT_DEPTH
+ * @default 8
+ * @since 1.3.0
+ */
+ j$.MAX_PRETTY_PRINT_DEPTH = 8;
+ /**
+ * Maximum number of array elements to display when pretty printing objects.
+ * This will also limit the number of keys and values displayed for an object.
+ * Elements past this number will be ellipised.
+ * @name jasmine.MAX_PRETTY_PRINT_ARRAY_LENGTH
+ * @default 50
+ * @since 2.7.0
+ */
+ j$.MAX_PRETTY_PRINT_ARRAY_LENGTH = 50;
+ /**
+ * Maximum number of characters to display when pretty printing objects.
+ * Characters past this number will be ellipised.
+ * @name jasmine.MAX_PRETTY_PRINT_CHARS
+ * @default 100
+ * @since 2.9.0
+ */
+ j$.MAX_PRETTY_PRINT_CHARS = 1000;
+ /**
+ * Default number of milliseconds Jasmine will wait for an asynchronous spec,
+ * before, or after function to complete. This can be overridden on a case by
+ * case basis by passing a time limit as the third argument to {@link it},
+ * {@link beforeEach}, {@link afterEach}, {@link beforeAll}, or
+ * {@link afterAll}. The value must be no greater than the largest number of
+ * milliseconds supported by setTimeout, which is usually 2147483647.
+ *
+ * While debugging tests, you may want to set this to a large number (or pass
+ * a large number to one of the functions mentioned above) so that Jasmine
+ * does not move on to after functions or the next spec while you're debugging.
+ * @name jasmine.DEFAULT_TIMEOUT_INTERVAL
+ * @default 5000
+ * @since 1.3.0
+ */
+ let DEFAULT_TIMEOUT_INTERVAL = 5000;
+ Object.defineProperty(j$, 'DEFAULT_TIMEOUT_INTERVAL', {
+ get: function() {
+ return DEFAULT_TIMEOUT_INTERVAL;
+ },
+ set: function(newValue) {
+ j$.util.validateTimeout(newValue, 'jasmine.DEFAULT_TIMEOUT_INTERVAL');
+ DEFAULT_TIMEOUT_INTERVAL = newValue;
+ }
+ });
+
+ j$.getGlobal = function() {
+ return jasmineGlobal;
+ };
+
+ /**
+ * Get the currently booted Jasmine Environment.
+ *
+ * @name jasmine.getEnv
+ * @since 1.3.0
+ * @function
+ * @return {Env}
+ */
+ j$.getEnv = function(options) {
+ const env = (j$.currentEnv_ = j$.currentEnv_ || new j$.Env(options));
+ //jasmine. singletons in here (setTimeout blah blah).
+ return env;
+ };
+
+ j$.isArray_ = function(value) {
+ return j$.isA_('Array', value);
+ };
+
+ j$.isObject_ = function(value) {
+ return (
+ !j$.util.isUndefined(value) && value !== null && j$.isA_('Object', value)
+ );
+ };
+
+ j$.isString_ = function(value) {
+ return j$.isA_('String', value);
+ };
+
+ j$.isNumber_ = function(value) {
+ return j$.isA_('Number', value);
+ };
+
+ j$.isFunction_ = function(value) {
+ return j$.isA_('Function', value);
+ };
+
+ j$.isAsyncFunction_ = function(value) {
+ return j$.isA_('AsyncFunction', value);
+ };
+
+ j$.isGeneratorFunction_ = function(value) {
+ return j$.isA_('GeneratorFunction', value);
+ };
+
+ j$.isTypedArray_ = function(value) {
+ return (
+ j$.isA_('Float32Array', value) ||
+ j$.isA_('Float64Array', value) ||
+ j$.isA_('Int16Array', value) ||
+ j$.isA_('Int32Array', value) ||
+ j$.isA_('Int8Array', value) ||
+ j$.isA_('Uint16Array', value) ||
+ j$.isA_('Uint32Array', value) ||
+ j$.isA_('Uint8Array', value) ||
+ j$.isA_('Uint8ClampedArray', value)
+ );
+ };
+
+ j$.isA_ = function(typeName, value) {
+ return j$.getType_(value) === '[object ' + typeName + ']';
+ };
+
+ j$.isError_ = function(value) {
+ if (!value) {
+ return false;
+ }
+
+ if (value instanceof Error) {
+ return true;
+ }
+
+ return typeof value.stack === 'string' && typeof value.message === 'string';
+ };
+
+ j$.isAsymmetricEqualityTester_ = function(obj) {
+ return obj ? j$.isA_('Function', obj.asymmetricMatch) : false;
+ };
+
+ j$.getType_ = function(value) {
+ return Object.prototype.toString.apply(value);
+ };
+
+ j$.isDomNode = function(obj) {
+ // Node is a function, because constructors
+ return typeof jasmineGlobal.Node !== 'undefined'
+ ? obj instanceof jasmineGlobal.Node
+ : obj !== null &&
+ typeof obj === 'object' &&
+ typeof obj.nodeType === 'number' &&
+ typeof obj.nodeName === 'string';
+ // return obj.nodeType > 0;
+ };
+
+ j$.isMap = function(obj) {
+ return (
+ obj !== null &&
+ typeof obj !== 'undefined' &&
+ obj.constructor === jasmineGlobal.Map
+ );
+ };
+
+ j$.isSet = function(obj) {
+ return (
+ obj !== null &&
+ typeof obj !== 'undefined' &&
+ obj.constructor === jasmineGlobal.Set
+ );
+ };
+
+ j$.isWeakMap = function(obj) {
+ return (
+ obj !== null &&
+ typeof obj !== 'undefined' &&
+ obj.constructor === jasmineGlobal.WeakMap
+ );
+ };
+
+ j$.isURL = function(obj) {
+ return (
+ obj !== null &&
+ typeof obj !== 'undefined' &&
+ obj.constructor === jasmineGlobal.URL
+ );
+ };
+
+ j$.isIterable_ = function(value) {
+ return value && !!value[Symbol.iterator];
+ };
+
+ j$.isDataView = function(obj) {
+ return (
+ obj !== null &&
+ typeof obj !== 'undefined' &&
+ obj.constructor === jasmineGlobal.DataView
+ );
+ };
+
+ j$.isPromise = function(obj) {
+ return !!obj && obj.constructor === jasmineGlobal.Promise;
+ };
+
+ j$.isPromiseLike = function(obj) {
+ return !!obj && j$.isFunction_(obj.then);
+ };
+
+ j$.fnNameFor = function(func) {
+ if (func.name) {
+ return func.name;
+ }
+
+ const matches =
+ func.toString().match(/^\s*function\s*(\w+)\s*\(/) ||
+ func.toString().match(/^\s*\[object\s*(\w+)Constructor\]/);
+
+ return matches ? matches[1] : '';
+ };
+
+ j$.isPending_ = function(promise) {
+ const sentinel = {};
+ return Promise.race([promise, Promise.resolve(sentinel)]).then(
+ function(result) {
+ return result === sentinel;
+ },
+ function() {
+ return false;
+ }
+ );
+ };
+
+ /**
+ * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}),
+ * that will succeed if the actual value being compared is an instance of the specified class/constructor.
+ * @name jasmine.any
+ * @since 1.3.0
+ * @function
+ * @param {Constructor} clazz - The constructor to check against.
+ */
+ j$.any = function(clazz) {
+ return new j$.Any(clazz);
+ };
+
+ /**
+ * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}),
+ * that will succeed if the actual value being compared is not `null` and not `undefined`.
+ * @name jasmine.anything
+ * @since 2.2.0
+ * @function
+ */
+ j$.anything = function() {
+ return new j$.Anything();
+ };
+
+ /**
+ * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}),
+ * that will succeed if the actual value being compared is `true` or anything truthy.
+ * @name jasmine.truthy
+ * @since 3.1.0
+ * @function
+ */
+ j$.truthy = function() {
+ return new j$.Truthy();
+ };
+
+ /**
+ * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}),
+ * that will succeed if the actual value being compared is `null`, `undefined`, `0`, `false` or anything falsey.
+ * @name jasmine.falsy
+ * @since 3.1.0
+ * @function
+ */
+ j$.falsy = function() {
+ return new j$.Falsy();
+ };
+
+ /**
+ * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}),
+ * that will succeed if the actual value being compared is empty.
+ * @name jasmine.empty
+ * @since 3.1.0
+ * @function
+ */
+ j$.empty = function() {
+ return new j$.Empty();
+ };
+
+ /**
+ * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher}
+ * that passes if the actual value is the same as the sample as determined
+ * by the `===` operator.
+ * @name jasmine.is
+ * @function
+ * @param {Object} sample - The value to compare the actual to.
+ */
+ j$.is = function(sample) {
+ return new j$.Is(sample);
+ };
+
+ /**
+ * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}),
+ * that will succeed if the actual value being compared is not empty.
+ * @name jasmine.notEmpty
+ * @since 3.1.0
+ * @function
+ */
+ j$.notEmpty = function() {
+ return new j$.NotEmpty();
+ };
+
+ /**
+ * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}),
+ * that will succeed if the actual value being compared contains at least the keys and values.
+ * @name jasmine.objectContaining
+ * @since 1.3.0
+ * @function
+ * @param {Object} sample - The subset of properties that _must_ be in the actual.
+ */
+ j$.objectContaining = function(sample) {
+ return new j$.ObjectContaining(sample);
+ };
+
+ /**
+ * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}),
+ * that will succeed if the actual value is a `String` that matches the `RegExp` or `String`.
+ * @name jasmine.stringMatching
+ * @since 2.2.0
+ * @function
+ * @param {RegExp|String} expected
+ */
+ j$.stringMatching = function(expected) {
+ return new j$.StringMatching(expected);
+ };
+
+ /**
+ * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}),
+ * that will succeed if the actual value is a `String` that contains the specified `String`.
+ * @name jasmine.stringContaining
+ * @since 3.10.0
+ * @function
+ * @param {String} expected
+ */
+ j$.stringContaining = function(expected) {
+ return new j$.StringContaining(expected);
+ };
+
+ /**
+ * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}),
+ * that will succeed if the actual value is an `Array` that contains at least the elements in the sample.
+ * @name jasmine.arrayContaining
+ * @since 2.2.0
+ * @function
+ * @param {Array} sample
+ */
+ j$.arrayContaining = function(sample) {
+ return new j$.ArrayContaining(sample);
+ };
+
+ /**
+ * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}),
+ * that will succeed if the actual value is an `Array` that contains all of the elements in the sample in any order.
+ * @name jasmine.arrayWithExactContents
+ * @since 2.8.0
+ * @function
+ * @param {Array} sample
+ */
+ j$.arrayWithExactContents = function(sample) {
+ return new j$.ArrayWithExactContents(sample);
+ };
+
+ /**
+ * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}),
+ * that will succeed if every key/value pair in the sample passes the deep equality comparison
+ * with at least one key/value pair in the actual value being compared
+ * @name jasmine.mapContaining
+ * @since 3.5.0
+ * @function
+ * @param {Map} sample - The subset of items that _must_ be in the actual.
+ */
+ j$.mapContaining = function(sample) {
+ return new j$.MapContaining(sample);
+ };
+
+ /**
+ * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}),
+ * that will succeed if every item in the sample passes the deep equality comparison
+ * with at least one item in the actual value being compared
+ * @name jasmine.setContaining
+ * @since 3.5.0
+ * @function
+ * @param {Set} sample - The subset of items that _must_ be in the actual.
+ */
+ j$.setContaining = function(sample) {
+ return new j$.SetContaining(sample);
+ };
+
+ /**
+ * Determines whether the provided function is a Jasmine spy.
+ * @name jasmine.isSpy
+ * @since 2.0.0
+ * @function
+ * @param {Function} putativeSpy - The function to check.
+ * @return {Boolean}
+ */
+ j$.isSpy = function(putativeSpy) {
+ if (!putativeSpy) {
+ return false;
+ }
+ return (
+ putativeSpy.and instanceof j$.SpyStrategy &&
+ putativeSpy.calls instanceof j$.CallTracker
+ );
+ };
+
+ /**
+ * Logs a message for use in debugging. If the spec fails, trace messages
+ * will be included in the {@link SpecResult|result} passed to the
+ * reporter's specDone method.
+ *
+ * This method should be called only when a spec (including any associated
+ * beforeEach or afterEach functions) is running.
+ * @function
+ * @name jasmine.debugLog
+ * @since 4.0.0
+ * @param {String} msg - The message to log
+ */
+ j$.debugLog = function(msg) {
+ j$.getEnv().debugLog(msg);
+ };
+
+ /**
+ * Replaces Jasmine's global error handling with a spy. This prevents Jasmine
+ * from treating uncaught exceptions and unhandled promise rejections
+ * as spec failures and allows them to be inspected using the spy's
+ * {@link Spy#calls|calls property} and related matchers such as
+ * {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}.
+ *
+ * After installing the spy, spyOnGlobalErrorsAsync immediately calls its
+ * argument, which must be an async or promise-returning function. The spy
+ * will be passed as the first argument to that callback. Normal error
+ * handling will be restored when the promise returned from the callback is
+ * settled.
+ *
+ * Note: The JavaScript runtime may deliver uncaught error events and unhandled
+ * rejection events asynchronously, especially in browsers. If the event
+ * occurs after the promise returned from the callback is settled, it won't
+ * be routed to the spy even if the underlying error occurred previously.
+ * It's up to you to ensure that the returned promise isn't resolved until
+ * all of the error/rejection events that you want to handle have occurred.
+ *
+ * You must await the return value of spyOnGlobalErrorsAsync.
+ * @name jasmine.spyOnGlobalErrorsAsync
+ * @function
+ * @async
+ * @param {AsyncFunction} fn - A function to run, during which the global error spy will be effective
+ * @example
+ * it('demonstrates global error spies', async function() {
+ * await jasmine.spyOnGlobalErrorsAsync(async function(globalErrorSpy) {
+ * setTimeout(function() {
+ * throw new Error('the expected error');
+ * });
+ * await new Promise(function(resolve) {
+ * setTimeout(resolve);
+ * });
+ * const expected = new Error('the expected error');
+ * expect(globalErrorSpy).toHaveBeenCalledWith(expected);
+ * });
+ * });
+ */
+ j$.spyOnGlobalErrorsAsync = async function(fn) {
+ await jasmine.getEnv().spyOnGlobalErrorsAsync(fn);
+ };
+};
+
+getJasmineRequireObj().util = function(j$) {
+ const util = {};
+
+ util.isUndefined = function(obj) {
+ return obj === void 0;
+ };
+
+ util.clone = function(obj) {
+ if (Object.prototype.toString.apply(obj) === '[object Array]') {
+ return obj.slice();
+ }
+
+ const cloned = {};
+ for (const prop in obj) {
+ if (obj.hasOwnProperty(prop)) {
+ cloned[prop] = obj[prop];
+ }
+ }
+
+ return cloned;
+ };
+
+ util.cloneArgs = function(args) {
+ return Array.from(args).map(function(arg) {
+ const str = Object.prototype.toString.apply(arg),
+ primitives = /^\[object (Boolean|String|RegExp|Number)/;
+
+ // All falsey values are either primitives, `null`, or `undefined.
+ if (!arg || str.match(primitives)) {
+ return arg;
+ } else if (str === '[object Date]') {
+ return new Date(arg.valueOf());
+ } else {
+ return j$.util.clone(arg);
+ }
+ });
+ };
+
+ util.getPropertyDescriptor = function(obj, methodName) {
+ let descriptor,
+ proto = obj;
+
+ do {
+ descriptor = Object.getOwnPropertyDescriptor(proto, methodName);
+ proto = Object.getPrototypeOf(proto);
+ } while (!descriptor && proto);
+
+ return descriptor;
+ };
+
+ util.has = function(obj, key) {
+ return Object.prototype.hasOwnProperty.call(obj, key);
+ };
+
+ util.errorWithStack = function errorWithStack() {
+ // Don't throw and catch. That makes it harder for users to debug their
+ // code with exception breakpoints, and it's unnecessary since all
+ // supported environments populate new Error().stack
+ return new Error();
+ };
+
+ function callerFile() {
+ const trace = new j$.StackTrace(util.errorWithStack());
+ return trace.frames[2].file;
+ }
+
+ util.jasmineFile = (function() {
+ let result;
+
+ return function() {
+ if (!result) {
+ result = callerFile();
+ }
+
+ return result;
+ };
+ })();
+
+ util.validateTimeout = function(timeout, msgPrefix) {
+ // Timeouts are implemented with setTimeout, which only supports a limited
+ // range of values. The limit is unspecified, as is the behavior when it's
+ // exceeded. But on all currently supported JS runtimes, setTimeout calls
+ // the callback immediately when the timeout is greater than 2147483647
+ // (the maximum value of a signed 32 bit integer).
+ const max = 2147483647;
+
+ if (timeout > max) {
+ throw new Error(
+ (msgPrefix || 'Timeout value') + ' cannot be greater than ' + max
+ );
+ }
+ };
+
+ return util;
+};
+
+getJasmineRequireObj().Spec = function(j$) {
+ function Spec(attrs) {
+ this.expectationFactory = attrs.expectationFactory;
+ this.asyncExpectationFactory = attrs.asyncExpectationFactory;
+ this.resultCallback = attrs.resultCallback || function() {};
+ this.id = attrs.id;
+ this.filename = attrs.filename;
+ this.parentSuiteId = attrs.parentSuiteId;
+ this.description = attrs.description || '';
+ this.queueableFn = attrs.queueableFn;
+ this.beforeAndAfterFns =
+ attrs.beforeAndAfterFns ||
+ function() {
+ return { befores: [], afters: [] };
+ };
+ this.userContext =
+ attrs.userContext ||
+ function() {
+ return {};
+ };
+ this.onStart = attrs.onStart || function() {};
+ this.autoCleanClosures =
+ attrs.autoCleanClosures === undefined ? true : !!attrs.autoCleanClosures;
+ this.getSpecName =
+ attrs.getSpecName ||
+ function() {
+ return '';
+ };
+ this.onLateError = attrs.onLateError || function() {};
+ this.catchingExceptions =
+ attrs.catchingExceptions ||
+ function() {
+ return true;
+ };
+ this.throwOnExpectationFailure = !!attrs.throwOnExpectationFailure;
+ this.timer = attrs.timer || new j$.Timer();
+
+ if (!this.queueableFn.fn) {
+ this.exclude();
+ }
+
+ this.reset();
+ }
+
+ Spec.prototype.addExpectationResult = function(passed, data, isError) {
+ const expectationResult = j$.buildExpectationResult(data);
+
+ if (passed) {
+ this.result.passedExpectations.push(expectationResult);
+ } else {
+ if (this.reportedDone) {
+ this.onLateError(expectationResult);
+ } else {
+ this.result.failedExpectations.push(expectationResult);
+
+ // TODO: refactor so that we don't need to override cached status
+ if (this.result.status) {
+ this.result.status = 'failed';
+ }
+ }
+
+ if (this.throwOnExpectationFailure && !isError) {
+ throw new j$.errors.ExpectationFailed();
+ }
+ }
+ };
+
+ Spec.prototype.setSpecProperty = function(key, value) {
+ this.result.properties = this.result.properties || {};
+ this.result.properties[key] = value;
+ };
+
+ Spec.prototype.execute = function(
+ queueRunnerFactory,
+ onComplete,
+ excluded,
+ failSpecWithNoExp
+ ) {
+ const onStart = {
+ fn: done => {
+ this.timer.start();
+ this.onStart(this, done);
+ }
+ };
+
+ const complete = {
+ fn: done => {
+ if (this.autoCleanClosures) {
+ this.queueableFn.fn = null;
+ }
+ this.result.status = this.status(excluded, failSpecWithNoExp);
+ this.result.duration = this.timer.elapsed();
+
+ if (this.result.status !== 'failed') {
+ this.result.debugLogs = null;
+ }
+
+ this.resultCallback(this.result, done);
+ },
+ type: 'specCleanup'
+ };
+
+ const fns = this.beforeAndAfterFns();
+
+ const runnerConfig = {
+ isLeaf: true,
+ queueableFns: [...fns.befores, this.queueableFn, ...fns.afters],
+ onException: e => this.handleException(e),
+ onMultipleDone: () => {
+ // Issue a deprecation. Include the context ourselves and pass
+ // ignoreRunnable: true, since getting here always means that we've already
+ // moved on and the current runnable isn't the one that caused the problem.
+ this.onLateError(
+ new Error(
+ 'An asynchronous spec, beforeEach, or afterEach function called its ' +
+ "'done' callback more than once.\n(in spec: " +
+ this.getFullName() +
+ ')'
+ )
+ );
+ },
+ onComplete: () => {
+ if (this.result.status === 'failed') {
+ onComplete(new j$.StopExecutionError('spec failed'));
+ } else {
+ onComplete();
+ }
+ },
+ userContext: this.userContext(),
+ runnableName: this.getFullName.bind(this)
+ };
+
+ if (this.markedPending || excluded === true) {
+ runnerConfig.queueableFns = [];
+ }
+
+ runnerConfig.queueableFns.unshift(onStart);
+ runnerConfig.queueableFns.push(complete);
+
+ queueRunnerFactory(runnerConfig);
+ };
+
+ Spec.prototype.reset = function() {
+ /**
+ * @typedef SpecResult
+ * @property {String} id - The unique id of this spec.
+ * @property {String} description - The description passed to the {@link it} that created this spec.
+ * @property {String} fullName - The full description including all ancestors of this spec.
+ * @property {String|null} parentSuiteId - The ID of the suite containing this spec, or null if this spec is not in a describe().
+ * @property {String} filename - The name of the file the spec was defined in.
+ * @property {Expectation[]} failedExpectations - The list of expectations that failed during execution of this spec.
+ * @property {Expectation[]} passedExpectations - The list of expectations that passed during execution of this spec.
+ * @property {Expectation[]} deprecationWarnings - The list of deprecation warnings that occurred during execution this spec.
+ * @property {String} pendingReason - If the spec is {@link pending}, this will be the reason.
+ * @property {String} status - Once the spec has completed, this string represents the pass/fail status of this spec.
+ * @property {number} duration - The time in ms used by the spec execution, including any before/afterEach.
+ * @property {Object} properties - User-supplied properties, if any, that were set using {@link Env#setSpecProperty}
+ * @property {DebugLogEntry[]|null} debugLogs - Messages, if any, that were logged using {@link jasmine.debugLog} during a failing spec.
+ * @since 2.0.0
+ */
+ this.result = {
+ id: this.id,
+ description: this.description,
+ fullName: this.getFullName(),
+ parentSuiteId: this.parentSuiteId,
+ filename: this.filename,
+ failedExpectations: [],
+ passedExpectations: [],
+ deprecationWarnings: [],
+ pendingReason: this.excludeMessage || '',
+ duration: null,
+ properties: null,
+ debugLogs: null
+ };
+ this.markedPending = this.markedExcluding;
+ this.reportedDone = false;
+ };
+
+ Spec.prototype.handleException = function handleException(e) {
+ if (Spec.isPendingSpecException(e)) {
+ this.pend(extractCustomPendingMessage(e));
+ return;
+ }
+
+ if (e instanceof j$.errors.ExpectationFailed) {
+ return;
+ }
+
+ this.addExpectationResult(
+ false,
+ {
+ matcherName: '',
+ passed: false,
+ expected: '',
+ actual: '',
+ error: e
+ },
+ true
+ );
+ };
+
+ /*
+ * Marks state as pending
+ * @param {string} [message] An optional reason message
+ */
+ Spec.prototype.pend = function(message) {
+ this.markedPending = true;
+ if (message) {
+ this.result.pendingReason = message;
+ }
+ };
+
+ /*
+ * Like {@link Spec#pend}, but pending state will survive {@link Spec#reset}
+ * Useful for fit, xit, where pending state remains.
+ * @param {string} [message] An optional reason message
+ */
+ Spec.prototype.exclude = function(message) {
+ this.markedExcluding = true;
+ if (this.message) {
+ this.excludeMessage = message;
+ }
+ this.pend(message);
+ };
+
+ Spec.prototype.getResult = function() {
+ this.result.status = this.status();
+ return this.result;
+ };
+
+ Spec.prototype.status = function(excluded, failSpecWithNoExpectations) {
+ if (excluded === true) {
+ return 'excluded';
+ }
+
+ if (this.markedPending) {
+ return 'pending';
+ }
+
+ if (
+ this.result.failedExpectations.length > 0 ||
+ (failSpecWithNoExpectations &&
+ this.result.failedExpectations.length +
+ this.result.passedExpectations.length ===
+ 0)
+ ) {
+ return 'failed';
+ }
+
+ return 'passed';
+ };
+
+ Spec.prototype.getFullName = function() {
+ return this.getSpecName(this);
+ };
+
+ Spec.prototype.addDeprecationWarning = function(deprecation) {
+ if (typeof deprecation === 'string') {
+ deprecation = { message: deprecation };
+ }
+ this.result.deprecationWarnings.push(
+ j$.buildExpectationResult(deprecation)
+ );
+ };
+
+ Spec.prototype.debugLog = function(msg) {
+ if (!this.result.debugLogs) {
+ this.result.debugLogs = [];
+ }
+
+ /**
+ * @typedef DebugLogEntry
+ * @property {String} message - The message that was passed to {@link jasmine.debugLog}.
+ * @property {number} timestamp - The time when the entry was added, in
+ * milliseconds from the spec's start time
+ */
+ this.result.debugLogs.push({
+ message: msg,
+ timestamp: this.timer.elapsed()
+ });
+ };
+
+ const extractCustomPendingMessage = function(e) {
+ const fullMessage = e.toString(),
+ boilerplateStart = fullMessage.indexOf(Spec.pendingSpecExceptionMessage),
+ boilerplateEnd =
+ boilerplateStart + Spec.pendingSpecExceptionMessage.length;
+
+ return fullMessage.slice(boilerplateEnd);
+ };
+
+ Spec.pendingSpecExceptionMessage = '=> marked Pending';
+
+ Spec.isPendingSpecException = function(e) {
+ return !!(
+ e &&
+ e.toString &&
+ e.toString().indexOf(Spec.pendingSpecExceptionMessage) !== -1
+ );
+ };
+
+ /**
+ * @interface Spec
+ * @see Configuration#specFilter
+ * @since 2.0.0
+ */
+ Object.defineProperty(Spec.prototype, 'metadata', {
+ get: function() {
+ if (!this.metadata_) {
+ this.metadata_ = {
+ /**
+ * The unique ID of this spec.
+ * @name Spec#id
+ * @readonly
+ * @type {string}
+ * @since 2.0.0
+ */
+ id: this.id,
+
+ /**
+ * The description passed to the {@link it} that created this spec.
+ * @name Spec#description
+ * @readonly
+ * @type {string}
+ * @since 2.0.0
+ */
+ description: this.description,
+
+ /**
+ * The full description including all ancestors of this spec.
+ * @name Spec#getFullName
+ * @function
+ * @returns {string}
+ * @since 2.0.0
+ */
+ getFullName: this.getFullName.bind(this)
+ };
+ }
+
+ return this.metadata_;
+ }
+ });
+
+ return Spec;
+};
+
+getJasmineRequireObj().Order = function() {
+ function Order(options) {
+ this.random = 'random' in options ? options.random : true;
+ const seed = (this.seed = options.seed || generateSeed());
+ this.sort = this.random ? randomOrder : naturalOrder;
+
+ function naturalOrder(items) {
+ return items;
+ }
+
+ function randomOrder(items) {
+ const copy = items.slice();
+ copy.sort(function(a, b) {
+ return jenkinsHash(seed + a.id) - jenkinsHash(seed + b.id);
+ });
+ return copy;
+ }
+
+ function generateSeed() {
+ return String(Math.random()).slice(-5);
+ }
+
+ // Bob Jenkins One-at-a-Time Hash algorithm is a non-cryptographic hash function
+ // used to get a different output when the key changes slightly.
+ // We use your return to sort the children randomly in a consistent way when
+ // used in conjunction with a seed
+
+ function jenkinsHash(key) {
+ let hash, i;
+ for (hash = i = 0; i < key.length; ++i) {
+ hash += key.charCodeAt(i);
+ hash += hash << 10;
+ hash ^= hash >> 6;
+ }
+ hash += hash << 3;
+ hash ^= hash >> 11;
+ hash += hash << 15;
+ return hash;
+ }
+ }
+
+ return Order;
+};
+
+getJasmineRequireObj().Env = function(j$) {
+ /**
+ * @class Env
+ * @since 2.0.0
+ * @classdesc The Jasmine environment.
+ * _Note:_ Do not construct this directly. You can obtain the Env instance by
+ * calling {@link jasmine.getEnv}.
+ * @hideconstructor
+ */
+ function Env(options) {
+ options = options || {};
+
+ const self = this;
+ const global = options.global || j$.getGlobal();
+
+ const realSetTimeout = global.setTimeout;
+ const realClearTimeout = global.clearTimeout;
+ const clearStack = j$.getClearStack(global);
+ this.clock = new j$.Clock(
+ global,
+ function() {
+ return new j$.DelayedFunctionScheduler();
+ },
+ new j$.MockDate(global)
+ );
+
+ const globalErrors = new j$.GlobalErrors();
+ const installGlobalErrors = (function() {
+ let installed = false;
+ return function() {
+ if (!installed) {
+ globalErrors.install();
+ installed = true;
+ }
+ };
+ })();
+
+ const runableResources = new j$.RunableResources({
+ getCurrentRunableId: function() {
+ const r = runner.currentRunable();
+ return r ? r.id : null;
+ },
+ globalErrors
+ });
+
+ let reporter;
+ let topSuite;
+ let runner;
+ let parallelLoadingState = null; // 'specs', 'helpers', or null for non-parallel
+
+ /**
+ * This represents the available options to configure Jasmine.
+ * Options that are not provided will use their default values.
+ * @see Env#configure
+ * @interface Configuration
+ * @since 3.3.0
+ */
+ const config = {
+ /**
+ * Whether to randomize spec execution order
+ * @name Configuration#random
+ * @since 3.3.0
+ * @type Boolean
+ * @default true
+ */
+ random: true,
+ /**
+ * Seed to use as the basis of randomization.
+ * Null causes the seed to be determined randomly at the start of execution.
+ * @name Configuration#seed
+ * @since 3.3.0
+ * @type (number|string)
+ * @default null
+ */
+ seed: null,
+ /**
+ * Whether to stop execution of the suite after the first spec failure
+ *
+ *
In parallel mode, `stopOnSpecFailure` works on a "best effort"
+ * basis. Jasmine will stop execution as soon as practical after a failure
+ * but it might not be immediate.