<|start_filename|>notes/database/flavour_parse.awk<|end_filename|>
# Note that I have preprocessed all embedded double quotes to the
# character sequence ~~, so that awk can correctly separate the
# CSV into fields
BEGIN {
  FPAT = "([^,]+)|(\"([^\"]+)\")"
}

{
  gsub("[[:cntrl:]]", "");
  if ($4 != "") {
    print "UPDATE button SET flavor_text=" $4 " WHERE name=\"" $2 "\";";
  }
}



<|start_filename|>test/src/ui/js/dummy2.js<|end_filename|>
if (!(Dummy)) {
  var Dummy = { };
}

Dummy.Two = 2;


<|start_filename|>test/src/ui/js/test_Buttons.js<|end_filename|>
module("Buttons", {
  'setup': function(assert) {
    BMTestUtils.ButtonsPre = BMTestUtils.getAllElements();

    // Back up any methods that we might decide to replace with mocks
    BMTestUtils.ApiBackup = { };
    BMTestUtils.CopyAllMethods(Api, BMTestUtils.ApiBackup);

    BMTestUtils.setupFakeLogin();

    // Create the buttons_page div so functions have something to modify
    if (document.getElementById('buttons_page') == null) {
      $('body').append($('<div>', {'id': 'env_message', }));
      $('body').append($('<div>', {'id': 'buttons_page', }));
    }

    Login.pageModule = { 'bodyDivId': 'buttons_page' };
  },
  'teardown': function(assert) {
    // Do not ignore intermittent failures in this test --- you
    // risk breaking the entire suite in hard-to-debug ways
    assert.equal(jQuery.active, 0,
      "All test functions MUST complete jQuery activity before exiting");

    // Delete all elements we expect this module to create

    // JavaScript variables
    delete Api.button;
    delete Api.buttonSet;
    delete Env.window.location.search;
    delete Buttons.buttonName;
    delete Buttons.setName;
    delete Buttons.page;

    Login.pageModule = null;

    // Page elements
    $('#buttons_page').remove();

    BMTestUtils.deleteEnvMessage();
    BMTestUtils.cleanupFakeLogin();

    // Restore any methods that we might have replaced with mocks
    BMTestUtils.CopyAllMethods(BMTestUtils.ApiBackup, Api);

    // Fail if any other elements were added or removed
    BMTestUtils.ButtonsPost = BMTestUtils.getAllElements();
    assert.deepEqual(
      BMTestUtils.ButtonsPost, BMTestUtils.ButtonsPre,
      "After testing, the page should have no unexpected element changes");
  }
});

// pre-flight test of whether the Buttons module has been loaded
test("test_Buttons_is_loaded", function(assert) {
  assert.ok(Buttons, "The Buttons namespace exists");
});

test("test_Buttons.showLoggedInPage", function(assert) {
  expect(3); // Tests plus teardown 2 tests

  Api.getButtonSetData = function(passedSetName) {
    assert.equal(passedSetName, null,
      'No set name should get passed to Api.getButtonSetData');
  };

  Api.getButtonData = function() {
    assert.ok(false, 'Api.getButtonData should not be invoked');
  };

  Buttons.showLoggedInPage();
});

test("test_Buttons.showLoggedInPage_set", function(assert) {
  expect(3); // Tests plus 2 teardown tests

  var expectedSetName = 'Soldiers';
  Env.window.location.search = '?set=' + expectedSetName;

  Api.getButtonSetData = function(passedSetName) {
    assert.equal(passedSetName, expectedSetName,
      'Set name should get passed to Api.getButtonSetData');
  };

  Api.getButtonData = function() {
    assert.ok(false, 'Api.getButtonData should not be invoked');
  };

  Buttons.showLoggedInPage();
});

test("test_Buttons.showLoggedInPage_button", function(assert) {
  expect(3); // Tests plus 2 teardown tests

  var expectedButtonName = 'Avis';
  Env.window.location.search = '?button=' + expectedButtonName;

  Api.getButtonData = function(passedButtonName) {
    assert.equal(passedButtonName, expectedButtonName,
      'Button name should get passed to Api.getButtonData');
  };

  Api.getButtonSetData = function() {
    assert.ok(false, 'Api.getButtonSetData should not be invoked');
  };

  Buttons.showLoggedInPage();
});

test("test_Buttons.showButton", function(assert) {
  Buttons.buttonName = 'Avis';

  Api.button = {
    'load_status': 'ok',
    'list': {
      'Avis': {
        'buttonName': 'Avis',
        'hasUnimplementedSkill': false,
        'recipe': '(4) (4) (10) (12) (X)',
        'buttonSet': 'Soldiers',
        'dieTypes': {
          'X Swing': {
            'code': 'X',
            'swingMin': 4,
            'swingMax': 20,
            'description':
                'X Swing Dice can be any die between 4 and 20. Swing Dice ' +
                'are allowed to be any integral size between their upper and ' +
                'lower limit, including both ends, and including nonstandard ' +
                'die sizes like 17 or 9. Each player chooses his or her ' +
                'Swing Die in secret at the beginning of the match, and ' +
                'thereafter the loser of each round may change their Swing ' +
                'Die between rounds. If a character has any two Swing Dice ' +
                'of the same letter, they must always be the same size.',
          },
        },
        'dieSkills': [ ],
        'isTournamentLegal': true,
        'artFilename': 'avis.png',
        'tags': [ ],
        'flavorText': null,
        'specialText': null,
      },
    },
  };

  Buttons.showButton();

  var buttonName = $('div.singleButton div.buttonName');
  assert.equal(buttonName.text(), 'Avis',
    'Button name should be displayed in single button mode');
});

test("test_Buttons.showSet", function(assert) {
  Buttons.setName = 'Soldiers';

  Api.buttonSet = {
    'load_status': 'ok',
    'list': {
      'Soldiers': {
        'setName': 'Soldiers',
        'buttons': {
          'Avis': {
            'buttonName': 'Avis',
            'hasUnimplementedSkill': false,
            'recipe': '(4) (4) (10) (12) (X)',
            'buttonSet': 'Soldiers',
            'dieSkills': [ ],
            'isTournamentLegal': true,
            'artFilename': 'avis.png',
            'tags': [ ],
          },
          'Kublai': {
            'buttonName': 'Kublai',
            'hasUnimplementedSkill': false,
            'recipe': '(4) (8) (12) (20) (X)',
            'buttonSet': 'Soldiers',
            'dieSkills': [ ],
            'isTournamentLegal': true,
            'artFilename': 'kublai.png',
            'tags': [ ],
          },
        },
      },
    },
  };

  Buttons.showSet();

  var setName = $('div.singleSet > h2');
  var buttonNames = $('div.singleSet a.buttonName');
  assert.equal(setName.text(), 'Soldiers',
    'Set name should be displayed in single set mode');
  assert.equal(buttonNames.length, 2,
    'All buttons in set should be displayed in single set mode');
});

test("test_Buttons.showSetList", function(assert) {
  Api.buttonSet = {
    'load_status': 'ok',
    'list': {
      'Soldiers': {
        'setName': 'Soldiers',
        'numberOfButtons': '13',
        'dieSkills': [ ],
        'dieTypes': [ 'X Swing' ],
        'onlyHasUnimplementedButtons': false,
      },
      'Fantasy': {
        'setName': 'Fantasy',
        'numberOfButtons': '14',
        'dieSkills': [ ],
        'dieTypes': [ 'Option' ],
        'onlyHasUnimplementedButtons': false,
      },
      'Samurai': {
        'setName': 'Samurai',
        'numberOfButtons': '7',
        'dieSkills': [ 'Focus' ],
        'dieTypes': [ 'V Swing', 'X Swing' ],
        'onlyHasUnimplementedButtons': false,
      },
    },
  };

  Buttons.showSetList();

  var setNames = $('div.allSets a.buttonSetLink');
  assert.equal(setNames.length, 3, 'All sets should be displayed in all sets mode');
});

test("test_Buttons.buildButtonBox", function(assert) {
  Buttons.buttonName = 'Avis';
  var button = {
    'buttonName': 'Avis',
    'hasUnimplementedSkill': false,
    'recipe': '(4) (4) (10) (12) (X)',
    'buttonSet': 'Soldiers',
    'dieSkills': [ ],
    'isTournamentLegal': true,
    'artFilename': 'avis.png',
    'tags': [ ],
  };

  var buttonBox = Buttons.buildButtonBox(button);

  var img = buttonBox.find('img');
  assert.ok(img.attr('src').match(/avis\.png/),
    'Button box should contain the correct image');
  var name = buttonBox.find('.buttonName');
  assert.equal(name.text(), 'Avis',
    'Button box should contain the correct name');
});


<|start_filename|>test/src/ui/js/test_Config.js<|end_filename|>
module("Config", {
  'setup': function() {
    BMTestUtils.ConfigPre = BMTestUtils.getAllElements();
  },
  'teardown': function(assert) {

    // Do not ignore intermittent failures in this test --- you
    // risk breaking the entire suite in hard-to-debug ways
    assert.equal(jQuery.active, 0,
      "All test functions MUST complete jQuery activity before exiting");

    // Delete all elements we expect this module to create

    Config.siteType = 'production';

    // Fail if any other elements were added or removed
    BMTestUtils.ConfigPost = BMTestUtils.getAllElements();
    assert.deepEqual(
      BMTestUtils.ConfigPost, BMTestUtils.ConfigPre,
      "After testing, the page should have no unexpected element changes");
  }
});

// pre-flight test of whether the Api module has been loaded
test("test_Config_is_loaded", function(assert) {
  expect(3); // number of tests plus 2 for the teardown test
  assert.ok(Config, "The Config namespace exists");
});


<|start_filename|>test/src/ui/js/test_History.js<|end_filename|>
module("History", {
  'setup': function() {
    BMTestUtils.HistoryPre = BMTestUtils.getAllElements();

    BMTestUtils.setupFakeLogin();

    // Create the history_page div so functions have something to modify
    if (document.getElementById('history_page') == null) {
      $('body').append($('<div>', {'id': 'env_message', }));
      $('body').append($('<div>', {'id': 'history_page', }));
    }
  },
  'teardown': function(assert) {

    // Do not ignore intermittent failures in this test --- you
    // risk breaking the entire suite in hard-to-debug ways
    assert.equal(jQuery.active, 0,
      "All test functions MUST complete jQuery activity before exiting");

    // Delete all elements we expect this module to create

    // JavaScript variables
    delete Env.window.location.href;
    delete Env.window.location.search;
    delete Env.window.location.hash;
    delete Env.history.state;
    delete Api.game_history;
    delete Api.player;
    delete Api.button;
    delete History.searchParameters;
    delete History.page;

    History.searchParameterInfo.playerNameA.source = { };
    History.searchParameterInfo.buttonNameA.source = { };
    History.searchParameterInfo.buttonNameB.source = { };
    History.searchParameterInfo.playerNameB.source = { };

    // Page elements
    $('#history_page').remove();
    $('#ui-datepicker-div').remove();

    BMTestUtils.deleteEnvMessage();
    BMTestUtils.cleanupFakeLogin();

    // Fail if any other elements were added or removed
    BMTestUtils.HistoryPost = BMTestUtils.getAllElements();
    assert.deepEqual(
      BMTestUtils.HistoryPost, BMTestUtils.HistoryPre,
      "After testing, the page should have no unexpected element changes");
  }
});

// pre-flight test of whether the History module has been loaded
test("test_History_is_loaded", function(assert) {
  assert.ok(History, "The History namespace exists");
});

// The purpose of this test is to demonstrate that the flow of
// History.showLoggedInPage() is correct for a showXPage function, namely
// that it calls an API getter with a showStatePage function as a
// callback.
//
// Accomplish this by mocking the invoked functions
test("test_History.showLoggedInPage", function(assert) {
  expect(6);
  var cached_getFilters = History.getFilters;
  var cached_getHistory = History.getHistory;
  var cached_showStatePage = History.showPage;
  var getFiltersCalled = false;
  var getHistoryCalled = false;
  History.showPage = function() {
    assert.ok(getFiltersCalled, "History.getFilters is called before History.showPage");
    assert.ok(getHistoryCalled, "History.getHistory is called before History.showPage");
  };
  History.getFilters = function(callback) {
    getFiltersCalled = true;
    assert.ok(true, "History.getFilters is called");
    callback();
  };
  History.getHistory = function(callback) {
    getHistoryCalled = true;
    assert.equal(callback, History.showPage,
      "History.getHistory is called with History.showPage as an argument");
    callback();
  };

  History.showLoggedInPage();

  History.getFilters = cached_getFilters;
  History.getHistory = cached_getHistory;
  History.showPage = cached_showStatePage;
});

test("test_History.getHistory", function(assert) {
  stop();
  History.searchParameters = {
    'sortColumn': 'lastMove',
    'sortDirection': 'DESC',
    'numberOfResults': '20',
    'page': '1',
    'playerNameA': 'tester',
    'status': 'COMPLETE',
  };
  History.searchParameterInfo.playerNameA.source = { 'tester': { }, };

  History.getHistory(function() {
    assert.ok(Api.game_history.games, "games list is parsed from server");
    assert.ok(Api.game_history.summary, "summary data is parsed from server");
    start();
  });
});

test("test_History.getFilters", function(assert) {
  stop();
  History.getFilters(function() {
    assert.ok(Api.player.list, "The list of players should be loaded");
    assert.ok(Api.button.list, "The list of players should be loaded");
    start();
  });
});

test("test_History.showPage", function(assert) {
  History.searchParameters = {
    'sortColumn': 'lastMove',
    'sortDirection': 'DESC',
    'numberOfResults': '20',
    'page': '1',
    'playerNameA': 'tester',
    'status': 'COMPLETE',
  };
  History.searchParameterInfo.playerNameA.source = { 'tester': { }, };

  // showPage is apparently just too amazing for qunit to handle testing
  // it asynchronously
  $.ajaxSetup({ async: false });
  History.getHistory(function() {
    History.getFilters(History.showPage);
    var htmlout = History.page.html();
    assert.ok(htmlout.length > 0,
       "The created page should have nonzero contents");
  });
  $.ajaxSetup({ async: true });
});

test("test_History.performManualSearch", function(assert) {
  // There are AJAX calls within the method that we can't pass a callback to,
  // so we need to make them run async in order for this to work reliably
  $.ajaxSetup({ async: false });
  History.page = $('<div>');
  History.performManualSearch();
  assert.ok(Api.game_history.games, "games list is parsed from server");
  assert.ok(Api.game_history.summary, "summary data is parsed from server");
  $.ajaxSetup({ async: true });
});

test("test_History.performAutomaticSearch", function(assert) {
  // There are AJAX calls within the method that we can't pass a callback to,
  // so we need to make them run async in order for this to work reliably
  $.ajaxSetup({ async: false });
  History.page = $('<div>');
  Env.history.pushState({ }, null, '#!hash');
  History.performAutomaticSearch();
  assert.ok(Api.game_history.games, "games list is parsed from server");
  assert.ok(Api.game_history.summary, "summary data is parsed from server");
  $.ajaxSetup({ async: true });
});

test("test_History.readSearchParametersFromUrl", function(assert) {
  Env.window.location.search = '';
  Env.window.location.hash =
    '#!playerNameA=tester&buttonNameB=Bunnies&WinningPlayer=A';
  History.readSearchParametersFromUrl();
  assert.equal(History.searchParameters.buttonNameB, 'Bunnies',
    "search parameters should be correctly read from the URL");
});

test("test_History.readSearchParametersFromForm", function(assert) {
  History.page = $('<div>');
  History.page.append($('<input>', {
    'id': 'parameter_playerNameA',
    'value': 'tester2'
  }));
  History.readSearchParametersFromForm();
  assert.equal(History.searchParameters.playerNameA, 'tester2',
    "search parameters should be correctly read from the form");
});

test("test_History.assignSearchParameter", function(assert) {
  History.searchParameters = { };
  var expected = 'Tied';
  History.assignSearchParameter('winningPlayer', 'string', expected);
  assert.equal(History.searchParameters.winningPlayer, expected,
    "search parameter value should be correctly assigned");
});

test("test_History.writeSearchParametersToUrl", function(assert) {
  History.searchParameters = {
    'playerNameA': 'tester',
    'numberOfResults': 50,
  };
  History.writeSearchParametersToUrl();
  assert.equal(Env.window.location.hash, '#!playerNameA=tester&numberOfResults=50',
    "search parameters should be correctly written to the URL");
});

test("test_History.buildSearchButtonDiv", function(assert) {
  History.page = $('<div>');
  History.page.append($('<input>', {
    'id': 'parameter_page',
    'type': 'text',
  }));
  var div = History.buildSearchButtonDiv();
  var button = div.find('#searchButton');
  assert.ok(button.length > 0, "Search button is created");
});

test("test_History.buildHiddenFields", function(assert) {
  var div = History.buildHiddenFields();
  var button = div.find('#parameter_page');
  assert.ok(button.length > 0, "Page number field is created");
});

test("test_History.buildResultsTableHeader", function(assert) {
  stop();
  History.getFilters(function() {
    var thead = History.buildResultsTableHeader();

    assert.ok(thead.find('th').length > 0, "Header cells are created");
    assert.ok(thead.find('th:contains("Game #")').length == 1,
      "Game # header exists");
    start();
  });
});

test("test_History.buildResultsTableBody", function(assert) {
  stop();
  History.searchParameters = {
    'sortColumn': 'lastMove',
    'sortDirection': 'DESC',
    'numberOfResults': '20',
    'page': '1',
    'playerNameA': 'tester',
    'status': 'COMPLETE',
  };
  History.searchParameterInfo.playerNameA.source = { 'tester': { }, };

  History.getHistory(function() {
    var tbody = History.buildResultsTableBody();
    var avisCell = tbody.find('td:contains("haruspex")');
    assert.ok(avisCell.length > 0, 'Table body contains game information.');

    start();
  });
});

test("test_History.scoreCol", function(assert) {
  var game;
  var column;

  // james: there needs to be a test for new games when this is supported

  // test normal active games
  game = {
    'status' : 'ACTIVE',
    'roundsWonA' : 1,
    'roundsWonB' : 2,
    'roundsDrawn' : 4,
    'targetWins' : 5,
    'colorA' : '#aa0000',
    'colorB' : '#00bb00',
  };

  column = History.scoreCol(game);
  assert.ok(column.is('td'), 'active game score column has the correct type');
  assert.equal(
    '1/2/4 (5)',
    column.text(),
    'active game score column has the correct text'
  );
  assert.equal(
    'rgb(255, 255, 255)',
    column.css('background-color'),
    'active game score column has the correct color'
  );

  // test completed games
  game = {
    'status' : 'COMPLETED',
    'roundsWonA' : 1,
    'roundsWonB' : 3,
    'roundsDrawn' : 2,
    'targetWins' : 3,
    'colorA' : '#ff0000',
    'colorB' : '#00ff00',
  };

  column = History.scoreCol(game);
  assert.ok(
    column.is('td'),
    'completed game score column has the correct type'
  );
  assert.equal(
    '1/3/2 (3)',
    column.text(),
    'completed game score column has the correct text'
  );
  assert.equal(
    'rgb(0, 255, 0)',
    column.css('background-color'),
    'completed game score column has the correct color'
  );

  // test cancelled games
  game = {
    'status' : 'CANCELLED',
    'roundsWonA' : 0,
    'roundsWonB' : 0,
    'roundsDrawn' : 0,
    'targetWins' : 4,
    'colorA' : '#aaaaaa',
    'colorB' : '#bbbbbb',
  };

  column = History.scoreCol(game);
  assert.ok(column.is('td'), 'cancelled game score column has the correct type');
  assert.equal(
    '–/–/– (4)',
    column.text(),
    'cancelled game score column has the correct text'
  );
  assert.equal(
    'rgb(255, 255, 255)',
    column.css('background-color'),
    'cancelled game score column has the correct color'
  );
});

test("test_History.buildResultsTableFooter", function(assert) {
  stop();
  History.searchParameters = {
    'sortColumn': 'lastMove',
    'sortDirection': 'DESC',
    'numberOfResults': '20',
    'page': '1',
    'playerNameA': 'tester',
    'status': 'COMPLETE',
  };
  History.searchParameterInfo.playerNameA.source = { 'tester': { }, };

  History.getHistory(function() {
    var tfoot = History.buildResultsTableFooter();

    var scoreFound = false;
    tfoot.find('td').each(function() {
      if ($(this).text().match('\\d\\d?\\d?%')) {
        scoreFound = true;
      }
    });
    start();
  });
});


<|start_filename|>src/ui/help/index.html<|end_filename|>
<!DOCTYPE html>
<!--
    This document should be written in polyglot HTML5/XHTML5, and therefore
    should run when served either as text/html or application/xhtml+xml.
-->
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>

    <meta charset="utf-8" />
    <title>Button Men Online</title>
    <link rel="stylesheet" href="../gui.css" />
    <meta http-equiv="Cache-control" content="no-cache" />

</head>

<body>

<h1> Site-specific info </h1>

<p> We have a <a href="getting-started.html">tutorial to help you get started</a>
using this site. </p>

<p> Check out the <a href="skills.html">button skills reference page</a>
for details about how the various skills work, interact with each other,
work differently on this site than elsewhere, etc. </p>

<p> Learn more <a href="about.html">about this project and this site</a>. </p>

<p> There's a <a href="http://buttonweavers.wikia.com/">Buttonweavers wiki</a>
at Wikia, which has more community-maintained/contributed information. The
<a href="../forum.html">forums</a> on this site are also a good place to
engage with the community. </p>

<p> Other questions not covered here? Try the <a href="faq.html">FAQ</a>.

<h1> General info about Button Men </h1>

<p> <a href="http://www.cheapass.com/node/39">Button Men</a> is copyright
1999, 2016 <NAME> and Cheapass Games
(<a href="http://www.cheapass.com/">www.cheapass.com</a> and
<a href="http://www.beatpeopleup.com">www.beatpeopleup.com</a>),
and is used with permission. Other <a href="terms.html">terms and conditions</a>
may be forthcoming. </p>

<p> Longtime online Button Men player Spindisc has
<a href="http://www.randomdice.com/games/buttonmen/">a site with lots of
general info about the game</a>. </p>

<h1> Developer info </h1>

<p> This site has <a href="https://github.com/buttonmen-dev/">a project</a>
on <a href="https://github.com">GitHub</a>, and you can browse
<a href="https://github.com/buttonmen-dev/buttonmen/">the repository</a>
there. </p>

<p> If you'd like to contribute to the project, check out the
<a href="https://github.com/buttonmen-dev/buttonmen/wiki/Developer-guide">developer guide</a>.
You can also <a href="https://github.com/buttonmen-dev/buttonmen/issues">view and create tickets</a>
about bugs, feature requests, etc. Or just post to <a href="../forum.html">the forums</a>
if you don't have (and don't want) a GitHub account. </p>

</body>

</html>


<|start_filename|>test/src/ui/js/dummy1.js<|end_filename|>
if (!(Dummy)) {
  var Dummy = { };
}

Dummy.One = 1;


<|start_filename|>test/src/ui/js/test_OpenGames.js<|end_filename|>
module("OpenGames", {
  'setup': function() {
    BMTestUtils.OpenGamesPre = BMTestUtils.getAllElements();

    BMTestUtils.setupFakeLogin();

    // Create the opengames_page div so functions have something to modify
    if (document.getElementById('opengames_page') == null) {
      $('body').append($('<div>', {'id': 'env_message', }));
      $('body').append($('<div>', {'id': 'opengames_page', }));
    }
  },
  'teardown': function(assert) {

    // Do not ignore intermittent failures in this test --- you
    // risk breaking the entire suite in hard-to-debug ways
    assert.equal(jQuery.active, 0,
      "All test functions MUST complete jQuery activity before exiting");

    // Delete all elements we expect this module to create

    // JavaScript variables
    delete Api.button;
    delete Api.open_games;
    delete Api.join_game_result;
    delete Api.cancel_game_result;
    delete OpenGames.page;

    // Page elements
    $('#opengames_page').remove();

    BMTestUtils.deleteEnvMessage();
    BMTestUtils.cleanupFakeLogin();

    // Fail if any other elements were added or removed
    BMTestUtils.OpenGamesPost = BMTestUtils.getAllElements();
    assert.deepEqual(
      BMTestUtils.OpenGamesPost, BMTestUtils.OpenGamesPre,
      "After testing, the page should have no unexpected element changes");
  }
});

// pre-flight test of whether the OpenGames module has been loaded
test("test_OpenGames_is_loaded", function(assert) {
  assert.ok(OpenGames, "The OpenGames namespace exists");
});

// The purpose of this test is to demonstrate that the flow of
// OpenGames.showLoggedInPage() is correct for a showXPage function, namely
// that it calls an API getter with a showStatePage function as a
// callback.
//
// Accomplish this by mocking the invoked functions
test("test_OpenGames.showLoggedInPage", function(assert) {
  expect(5);
  var cached_getOpenGames = OpenGames.getOpenGames;
  var cached_showStatePage = OpenGames.showPage;
  var getOpenGamesCalled = false;
  OpenGames.showPage = function() {
    assert.ok(getOpenGamesCalled, "OpenGames.getOpenGames is called before OpenGames.showPage");
  };
  OpenGames.getOpenGames = function(callback) {
    getOpenGamesCalled = true;
    assert.equal(callback, OpenGames.showPage,
      "OpenGames.getOpenGames is called with OpenGames.showPage as an argument");
    callback();
  };

  OpenGames.showLoggedInPage();
  var item = document.getElementById('opengames_page');
  assert.equal(item.nodeName, "DIV",
        "#opengames_page is a div after showLoggedInPage() is called");

  OpenGames.getOpenGames = cached_getOpenGames;
  OpenGames.showPage = cached_showStatePage;
});

test("test_OpenGames.getOpenGames", function(assert) {
  stop();
  OpenGames.getOpenGames(function() {
    assert.ok(Api.open_games, "open games are parsed from server");
    if (Api.open_games) {
      assert.equal(Api.open_games.load_status, 'ok',
        "open games are parsed successfully from server");
    }
    start();
  });
});

test("test_OpenGames.showPage", function(assert) {
  stop();
  OpenGames.getOpenGames(function() {
    OpenGames.showPage();
    var htmlout = OpenGames.page.html();
    assert.ok(htmlout.length > 0,
       "The created page should have nonzero contents");
    start();
  });
});

test("test_OpenGames.buildGameTable", function(assert) {
  stop();
  var buttons = {
    'Avis': {
      'recipe': 'Avis: (4) (4) (10) (12) (X)',
      'greyed': false,
    },
    'Apples': {
      'recipe': 'Apples: (8) (8) (2/12) (8/16) (20/24)',
      'greyed': false,
    },
    'Bauer': {
      'recipe': 'Bauer: (8) (10) (12) (20) (X)',
      'greyed': false,
    },
    'Stark': {
      'recipe': 'Stark: (4) (6) (8) (X) (X)',
      'greyed': false,
    },
    '<NAME>': {
      'recipe': 'Von Pinn: (4) p(6,6) (10) (20) (W)',
      'greyed': false,
    },
  };
  Api.getOpenGamesData(function() {
    var table = OpenGames.buildGameTable('joinable', buttons);
    assert.ok(table.find('td.gameAction').length > 0,
      "Table rows were generated");
    start();
  });
});

test("test_OpenGames.joinOpenGame", function(assert) {
  var gameId = 4400;

  var gameRow = $('<tr>');
  var gameActionTd = $('<td>');
  gameRow.append(gameActionTd);
  var joinButton = $('<button>', {
    'type': 'button',
    'text': 'Join Game ' + gameId,
    'data-gameId': gameId,
  });
  gameActionTd.append(joinButton);
  joinButton.click(OpenGames.joinOpenGame);

  // We can't inject our callback into the API call made by this event, so
  // we'll just make the call asynchronously and run our tests afterward
  $.ajaxSetup({ async: false });
  joinButton.click();
  $.ajaxSetup({ async: true });

  var gameLink = gameActionTd.find('a');
  assert.ok(gameLink.length > 0, "link to game was created");
});

test("test_OpenGames.displayJoinResult", function(assert) {
  var gameId = 21;

  var gameActionTd = $('<td>');
  var joinButton = $('<button>', {
    'type': 'button',
    'text': 'Join Game ' + gameId,
    'data-gameId': gameId,
  });
  gameActionTd.append(joinButton);

  var victimButtonTd = $('<td>', { 'class': 'victimButton', });
  var buttonSelect = $('<select>');
  victimButtonTd.append(buttonSelect);

  OpenGames.displayJoinResult(joinButton, buttonSelect, gameId, 'Avis');

  var buttonSpan = victimButtonTd.find('span');
  assert.ok(buttonSpan.length > 0, "button name is displayed");
  assert.equal(buttonSpan.text(), 'Avis', "correct button name is displayed");
});

test("test_OpenGames.cancelOpenGame", function(assert) {
  var gameId = 4900;

  var gameRow = $('<tr>');
  var gameActionTd = $('<td>');
  gameRow.append(gameActionTd);
  var cancelButton = $('<button>', {
    'type': 'button',
    'text': 'Cancel Game ' + gameId,
    'data-gameId': gameId,
  });
  gameActionTd.append(cancelButton);
  cancelButton.click(OpenGames.cancelOpenGame);

  // We can't inject our callback into the API call made by this event, so
  // we'll just make the call asynchronously and run our tests afterward
  $.ajaxSetup({ async: false });
  cancelButton.click();
  $.ajaxSetup({ async: true });

  var cancelSpan = gameActionTd.find('span');
  assert.ok(cancelSpan.length > 0, 'information span was created');
  assert.ok(!cancelButton.is(':visible'));
});

test("test_OpenGames.displayCancelResult", function(assert) {
  var gameId = 49;

  var gameActionTd = $('<td>');
  var cancelButton = $('<button>', {
    'type': 'button',
    'text': 'Cancel Game ' + gameId,
    'data-gameId': gameId,
  });
  gameActionTd.append(cancelButton);

  OpenGames.displayCancelResult(cancelButton, gameId);

  var cancelSpan = gameActionTd.find('span');
  assert.ok(cancelSpan.length > 0, 'information span was created');
  assert.equal(
    cancelSpan.text(),
    'Cancelled Game 49',
    'correct information is displayed'
  );
});


<|start_filename|>test/src/ui/js/test_ActivePlayers.js<|end_filename|>
module("ActivePlayers", {
  'setup': function() {
    BMTestUtils.ActivePlayersPre = BMTestUtils.getAllElements();

    BMTestUtils.setupFakeLogin();

    // Create the activeplayers_page div so functions have something to modify
    if (document.getElementById('activeplayers_page') == null) {
      $('body').append($('<div>', {'id': 'env_message', }));
      $('body').append($('<div>', {'id': 'activeplayers_page', }));
    }
  },
  'teardown': function(assert) {

    // Do not ignore intermittent failures in this test --- you
    // risk breaking the entire suite in hard-to-debug ways
    assert.equal(jQuery.active, 0,
      "All test functions MUST complete jQuery activity before exiting");

    // Delete all elements we expect this module to create

    // JavaScript variables
    delete Api.active_players;
    delete ActivePlayers.page;

    // Page elements
    $('#activeplayers_page').remove();

    BMTestUtils.deleteEnvMessage();
    BMTestUtils.cleanupFakeLogin();

    // Fail if any other elements were added or removed
    BMTestUtils.ActivePlayersPost = BMTestUtils.getAllElements();
    assert.deepEqual(
      BMTestUtils.ActivePlayersPost, BMTestUtils.ActivePlayersPre,
      "After testing, the page should have no unexpected element changes");
  }
});

// pre-flight test of whether the ActivePlayers module has been loaded
test("test_ActivePlayers_is_loaded", function(assert) {
  assert.ok(ActivePlayers, "The ActivePlayers namespace exists");
});

// The purpose of this test is to demonstrate that the flow of
// ActivePlayers.showLoggedInPage() is correct for a showXPage function, namely
// that it calls an API getter with a showStatePage function as a
// callback.
//
// Accomplish this by mocking the invoked functions
test("test_ActivePlayers.showLoggedInPage", function(assert) {
  expect(5);
  var cached_getActivePlayers = Api.getActivePlayers;
  var cached_showStatePage = ActivePlayers.showPage;
  var getActivePlayersCalled = false;
  ActivePlayers.showPage = function() {
    assert.ok(getActivePlayersCalled, "ActivePlayers.getActivePlayers is called before ActivePlayers.showPage");
  };
  Api.getActivePlayers = function(number, callback) {
    getActivePlayersCalled = true;
    assert.equal(callback, ActivePlayers.showPage,
      "ActivePlayers.getActivePlayers is called with ActivePlayers.showPage as an argument");
    callback();
  };

  ActivePlayers.showLoggedInPage();
  var item = document.getElementById('activeplayers_page');
  assert.equal(item.nodeName, "DIV",
        "#activeplayers_page is a div after showLoggedInPage() is called");
  Api.getActivePlayers = cached_getActivePlayers;
  ActivePlayers.showPage = cached_showStatePage;
});

test("test_ActivePlayers.getActivePlayers", function(assert) {
  stop();
  Api.getActivePlayers(50, function() {
    assert.ok(Api.active_players, "ActivePlayers info parsed from server");
    if (Api.active_players) {
      assert.equal(Api.active_players.load_status, 'ok',
        "ActivePlayers info parsed successfully from server");
    }
    start();
  });
});

test("test_ActivePlayers.showPage", function(assert) {
  stop();
  Api.getActivePlayers(50, function() {
    ActivePlayers.showPage();
    var htmlout = ActivePlayers.page.html();
    assert.ok(htmlout.length > 0,
       "The created page should have nonzero contents");
    start();
  });
});

test("test_ActivePlayers.buildPlayersTable", function(assert) {
  stop();
  Api.getActivePlayers(50, function() {
    var table = ActivePlayers.buildPlayersTable();
    var htmlout = table.html();
    assert.ok(htmlout.match('responder0'), "Players table content contains a username");
    assert.ok(htmlout.match(' seconds'), "Players table content contains a timestamp");
    start();
  });
});


<|start_filename|>src/ui/js/ActivePlayers.js<|end_filename|>
// namespace for this "module"
var ActivePlayers = {};

ActivePlayers.bodyDivId = 'activeplayers_page';

ActivePlayers.NUMBER_OF_ACTIVE_PLAYERS = 50;

////////////////////////////////////////////////////////////////////////
// Action flow through this page:
// * ActivePlayers.showLoggedInPage() is the landing function. Always call
//   this first. It calls the API, setting Api.active_players, and then
//   calls ActivePlayers.showPage()
// * ActivePlayers.showPage() uses the data returned by the API to build
//   the contents of the page as ActivePlayers.page and calls
//   Login.arrangePage()
////////////////////////////////////////////////////////////////////////

ActivePlayers.showLoggedInPage = function() {
  // Get all needed information, then display Active Players page
  Api.getActivePlayers(ActivePlayers.NUMBER_OF_ACTIVE_PLAYERS,
    ActivePlayers.showPage);
};

ActivePlayers.showPage = function() {
  ActivePlayers.page = $('<div>');

  if (Api.active_players.load_status != 'ok') {
    if (Env.message === undefined || Env.message === null) {
      Env.message = {
        'type': 'error',
        'text': 'An internal error occurred while loading the list of players.',
      };
    }
  } else if ((Api.active_players.players.length === 0)) {
    Env.message = {
      'type': 'none',
      'text': 'There are no players. You are the last human on Earth.',
    };
  } else {
    ActivePlayers.page.append($('<h2>', {'text': 'Who\'s Online', }));
    ActivePlayers.page.append(ActivePlayers.buildPlayersTable());
  }

  // Actually lay out the page
  Login.arrangePage(ActivePlayers.page);
};

////////////////////////////////////////////////////////////////////////
// Helper routines to add HTML entities to existing pages

ActivePlayers.buildPlayersTable = function() {
  var table = $('<table>', { 'class': 'activePlayerList', });

  var thead = $('<thead>');
  table.append(thead);
  var headerRow = $('<tr>');
  thead.append(headerRow);
  headerRow.append($('<th>', {
    'class': 'player',
    'text': 'Player',
  }));
  headerRow.append($('<th>', {
    'class': 'idleness',
    'text': 'Idle',
  }));

  var tbody = $('<tbody>');
  table.append(tbody);
  $.each(Api.active_players.players, function(index, playerData) {
    var playerRow = $('<tr>');
    tbody.append(playerRow);
    var profileLink = Env.buildProfileLink(playerData.playerName);
    playerRow.append($('<td>', {
      'class': 'player',
    }).append(profileLink));
    playerRow.append($('<td>', {
      'class': 'idleness',
      'text': playerData.idleness
    }));
  });

  return table;
};


<|start_filename|>src/ui/js/Loader.js<|end_filename|>
// namespace for this "module"
var Loader = {};

// These scripts are loaded by every page, so we have them here by default
Loader.defaultScripts = [
  'js/extern/jquery.cookie.js',
  'js/extern/moment.js',
];

// These BM modules are loaded by every page, and we autoversion them
// to avoid caching problems after deployments
Loader.defaultBMModules = [
  'Api',
  'Config',
  'Env',
  'Login',
];

Loader.loadScripts = function(scripts, callback, version) {
  // The function to be called when all the scripts have been loaded.
  Loader.callback = callback;

  // Tracks which scripts are still loading and which have finished
  Loader.loadStatus = { };
  $.each(Loader.defaultScripts, function(index, script) {
    Loader.loadStatus[script] = false;
  });
  $.each(Loader.defaultBMModules, function(index, module) {
    var script;
    if (version) {
      script = 'js/' + module + '.' + version + '.js';
    } else {
      script = 'js/' + module + '.js';
    }
    Loader.loadStatus[script] = false;
  });
  $.each(scripts, function(index, script) {
    Loader.loadStatus[script] = false;
  });

  // Load each script with $.getScript, passing it a callback that checks
  // whether or not it was the last script to finish loading
  $.each(Loader.loadStatus, function(script) {
    $.ajax({
      url: script,
      dataType: 'script',
      cache: true,
      success: function() {
        Loader.loadStatus[script] = true;
        var allScriptsAreLoaded = true;
        $.each(Loader.loadStatus, function(script, status) {
          if (!status) {
            allScriptsAreLoaded = false;
          }
        });
        // If this was the last one to finish, then start loading the page!
        if (allScriptsAreLoaded) {
          callback();
        }
      }
    });
  });
};


<|start_filename|>src/ui/js/History.js<|end_filename|>
// namespace for this "module"
var History = {};

History.bodyDivId = 'history_page';

// These are the parameters that we need to pass to the API. Having this
// information about them here helps to read them from the page, write them to
// the URL, build the table columns for them, etc.
History.searchParameterInfo = {
  'gameId': {
    'text': 'Game #',
    'inputType': 'text',
    'dataType': 'number',
    'toolTip': 'Search or sort by the game number',
  },
  'playerNameA': {
    'text': 'Player A',
    'inputType': 'select',
    'source': { },
    'dataType': 'string',
    'toolTip': 'Search or sort by one of the players in the game',
  },
  'buttonNameA': {
    'text': 'Button A',
    'inputType': 'select',
    'source': { },
    'dataType': 'string',
    'toolTip':
      'Search or sort by the button used by one of the players in the game',
  },
  'buttonNameB': {
    'text': 'Button B',
    'inputType': 'select',
    'source': { },
    'dataType': 'string',
    'toolTip': 'Search or sort by the button used by Player A\'s opponent',
  },
  'playerNameB': {
    'text': 'Player B',
    'inputType': 'select',
    'source': { },
    'dataType': 'string',
    'toolTip': 'Search or sort by Player A\'s opponent',
  },
  'gameStart': {
    'text': 'Game Start',
    'inputType': 'dateRange',
    'dataType': 'date',
    'toolTip': 'Search or sort by the date the game started',
  },
  'lastMove': {
    'text': 'Last Move',
    'inputType': 'dateRange',
    'dataType': 'date',
    'toolTip':
      'Search or sort by the date the most recent move was made in the game ' +
      '(for completed games, this is when the game ended)',
  },
  'winningPlayer': {
    'text': 'Round Score',
    'inputType': 'select',
    'source': {
      'A': 'A Winning',
      'B': 'B Winning',
      'Tie': 'Tie',
    },
    'dataType': 'string',
    'toolTip':
      'Search or sort by which player is closer to winning the game ' +
      '(for completed games, this is the player who won)',
  },
  'status': {
    'text': 'Game status',
    'inputType': 'select',
    'source': {
      'COMPLETE': 'Completed',
      'ACTIVE': 'In Progress',
      'UNSTARTED': 'Unstarted',
      'CANCELLED': 'Cancelled',
    },
    'dataType': 'string',
    'toolTip': 'Search or sort by the game status',
  },
  'sortColumn': {
    'inputType': 'hidden',
    'dataType': 'string',
    'defaultValue': 'lastMove',
  },
  'sortDirection': {
    'inputType': 'hidden',
    'dataType': 'string',
    'defaultValue': 'DESC',
  },
  'numberOfResults': {
    'inputType': 'special',
    'dataType': 'number',
    'source': {
      10: '10',
      20: '20',
      50: '50',
      100: '100'
    },
    'defaultValue': 20,
  },
  'page': {
    'inputType': 'hidden',
    'dataType': 'number',
    'defaultValue': 1,
  },
};

////////////////////////////////////////////////////////////////////////
// Primary flow through this page:
// * History.showLoggedInPage() is the landing function. Always call
//   this first. On the initial page load, it sets History.searchParameters
//   based on the hashbang of the incoming URL, then calls History.getHistory().
// * History.getFilters() gets data from the API to populate the filters
//   with. It sets Api.player and Api.button, then calls History.getHistory()
// * History.getHistory() calls the API, passing it the History.searchParameters
//   collection and causing Api.game_history to be set. It then calls
//   History.showPage()
// * History.showPage() uses the data returned by the API to build the contents
//   of the page as History.page and calls Login.arrangePage()
//
// Events:
// * History.performManualSearch() is called whenever the search button, a
//   sorting arrow or a paging link is clicked. It sets History.searchParameters
//   based on the form inputs, writes those parameters into the page history
//   and the hashbang URL, and then calls History.getHistory()
// * History.performAutomaticSearch() is called whenever the user clicks on the
//   forward or back button. It sets History.searchParameters based on the page
//   history state associated with the new URL and calls History.getHistory()
////////////////////////////////////////////////////////////////////////

History.showLoggedInPage = function() {
  // When the user hits the back button to retrace their path through the
  // hashbang URL's, load the search results that belong to that "page"
  $(window).bind('popstate', History.performAutomaticSearch);

  History.readSearchParametersFromUrl();
  if (History.searchParameters !== undefined) {
    // Since we just entered the page, we should set up a history state for
    // this URL, in case the user presses the back button
    Env.history.replaceState(History.searchParameters,
        'Button Men Online &mdash; History', Env.window.location.hash);

    // Get all needed information, then display History page
    History.getFilters(function() {
      History.getHistory(History.showPage);
    });
  } else {
    History.getFilters(History.showPage);
  }
};

History.getFilters = function(callback) {
  Env.callAsyncInParallel(
    [
      Api.getPlayerData,
      { 'func': Api.getButtonData, 'args': [ null ] },
    ], function() {
      var playerValues = { };
      $.each(Api.player.list, function(name, playerInfo) {
        if (playerInfo.status == 'ACTIVE') {
          playerValues[name] = name;
        }
      });
      History.searchParameterInfo.playerNameA.source = playerValues;
      History.searchParameterInfo.playerNameB.source = playerValues;

      var buttonValues = { };
      $.each(Api.button.list, function(name) {
        buttonValues[name] = name;
      });
      History.searchParameterInfo.buttonNameA.source = buttonValues;
      History.searchParameterInfo.buttonNameB.source = buttonValues;

      callback();
    });
};

History.getHistory = function(callback) {
  // Validate the search parameters that are supposed to be derived from set
  // lists of values (like player names).
  var validationError = '';
  $.each(History.searchParameterInfo, function(name, info) {
    if (History.searchParameters[name] !== undefined &&
      info.source !== undefined) {
      if (info.source[History.searchParameters[name]] === undefined) {
        validationError += name + ' is not recognized. ';
      }
    }
  });

  if (validationError) {
    Env.message = {
      'type': 'error',
      'text': validationError,
    };
    History.page = $('<div>');
    Login.arrangePage(History.page);
    return;
  }

  $('#searchButton').attr('disabled', 'disabled');

  // For the required fields, set the default values
  $.each(History.searchParameterInfo, function(name, info) {
    if (info.defaultValue !== undefined &&
      History.searchParameters[name] === undefined) {
      History.searchParameters[name] = info.defaultValue;
    }
  });

  Api.searchGameHistory(
    History.searchParameters,
    callback
  );
};

History.showPage = function() {
  History.page = $('<div>');

  if (Api.game_history !== undefined &&
    Api.game_history.load_status != 'ok') {
    // An error has occurred, and we've presumably already registered the
    // error message, so we should just display it.
    Login.arrangePage(History.page);
    return;
  }

  History.page.append(History.buildSearchButtonDiv());
  History.page.append($('<h2>', { 'text': 'Game History', }));
  History.page.append(History.buildHiddenFields());

  var resultsTable = $('<table>', { 'class': 'gameList', });
  History.page.append(resultsTable);

  // Display the column headers and search filters
  resultsTable.append(History.buildResultsTableHeader());

  if (Api.game_history !== undefined) {
    // List the games that were returned
    resultsTable.append(History.buildResultsTableBody());
    // Show summary data
    resultsTable.append(History.buildResultsTableFooter());
  }

  // Actually lay out the page
  Login.arrangePage(History.page);
};

History.performManualSearch = function() {
  History.readSearchParametersFromForm();
  if (History.searchParameters === undefined) {
    // A validation error must have occurred
    Env.showStatusMessage();
    return;
  }

  // We want a shareable URL that points to this particular set of search
  // results.
  History.writeSearchParametersToUrl();

  // Get all needed information, then display History page
  History.getFilters(function() {
    History.getHistory(History.showPage);
  });
};

History.performAutomaticSearch = function() {
  History.searchParameters = Env.history.state;
  if (History.searchParameters === undefined) {
    History.getFilters(History.showPage);
  }

  // Get all needed information, then display History page
  History.getFilters(function() {
    History.getHistory(History.showPage);
  });
};

////////////////////////////////////////////////////////////////////////
// Helper routines to read and write the search parameter values

History.readSearchParametersFromUrl = function() {
  if (Env.window.location.hash === '') {
    History.searchParameters = undefined;
    return;
  }

  History.searchParameters = { };
  $.each(History.searchParameterInfo, function(name, info) {
    if (info.inputType == 'dateRange') {
      var minValue = Env.getParameterByName(name + 'Min');
      History.assignSearchParameter(name + 'Min', info.dataType, minValue);
      var maxValue = Env.getParameterByName(name + 'Max');
      History.assignSearchParameter(name + 'Max', info.dataType, maxValue);
    } else {
      var value = Env.getParameterByName(name);
      History.assignSearchParameter(name, info.dataType, value);
    }
  });
};

History.readSearchParametersFromForm = function() {
  History.searchParameters = { };

  $.each(History.searchParameterInfo, function(name, info) {
    if (info.inputType == 'dateRange') {
      var minValue = History.page.find('#parameter_' + name + 'Min').val();
      History.assignSearchParameter(name + 'Min', info.dataType, minValue);
      var maxValue = History.page.find('#parameter_' + name + 'Max').val();
      History.assignSearchParameter(name + 'Max', info.dataType, maxValue);
    } else {
      var value = History.page.find('#parameter_' + name).val();
      History.assignSearchParameter(name, info.dataType, value);
    }
  });
};

History.assignSearchParameter = function(name, type, value) {
  // If we've already encountered one validation error, skip the rest
  if (History.searchParameters === undefined) {
    return;
  }

  if (value === undefined || value === null || value === '') {
    return;
  }

  if (type == 'number') {
    if (!value.match(/^\d+$/)) {
      Env.message = {
        'type': 'error',
        'text': name + ' must be a number.',
      };
      History.searchParameters = undefined;
      return;
    }
  } else if (type == 'date') {
    value = Env.parseDateTime(value, 'date');
    if (value === null) {
      Env.message = {
        'type': 'error',
        'text': name + ' is not a valid date.',
      };
      History.searchParameters = undefined;
      return;
    }
  }
  History.searchParameters[name] = value;
};

History.writeSearchParametersToUrl = function() {
  var parameterHash = '#!';
  $.each(History.searchParameterInfo, function(name, info) {
    var value;
    if (info.dataType == 'date') {
      if (History.searchParameters[name + 'Min'] !== undefined) {
        value = History.searchParameters[name + 'Min'];
        value = Env.formatTimestamp(value, 'date');
        parameterHash += name + 'Min' + '=' + encodeURIComponent(value) + '&';
      }
      if (History.searchParameters[name + 'Max'] !== undefined) {
        value = History.searchParameters[name + 'Max'];
        value = Env.formatTimestamp(value, 'date');
        parameterHash += name + 'Max' + '=' + encodeURIComponent(value) + '&';
      }
    } else {
      if (History.searchParameters[name] !== undefined &&
        History.searchParameters[name] !=
          History.searchParameterInfo[name].defaultValue) {
        value = History.searchParameters[name];
        parameterHash += name + '=' + encodeURIComponent(value) + '&';
      }
    }
  });

  // Trim off the trailing &
  parameterHash = parameterHash.replace(/&$/, '');
  Env.history.pushState(History.searchParameters,
    'Button Men Online &mdash; History', parameterHash);
};

////////////////////////////////////////////////////////////////////////
// Helper routines to generate HTML to populate the page

History.buildSearchButtonDiv = function() {
  var buttonDiv = $('<div>');

  buttonDiv.append($('<text>', { 'text': 'Display up to ', }));
  var pageSizeSelect = $('<select>', { 'id': 'parameter_numberOfResults', });
  buttonDiv.append(pageSizeSelect);
  $.each(History.searchParameterInfo.numberOfResults.source,
    function (value, name) {
      pageSizeSelect.append($('<option>', {
        'value': value,
        'text': name,
      }));
    });
  if (History.searchParameters === undefined ||
    History.searchParameters.numberOfResults === undefined) {
    pageSizeSelect
      .val(History.searchParameterInfo.numberOfResults.defaultValue);
  } else {
    pageSizeSelect.val(History.searchParameters.numberOfResults);
  }
  buttonDiv.append($('<text>', { 'text': ' games per page: ', }));

  var searchButton = $('<input>', {
    'type': 'button',
    'id': 'searchButton',
    'value': 'Search',
  });
  buttonDiv.append(searchButton);
  searchButton.click(function() {
    History.page.find('#parameter_page')
      .val(History.searchParameterInfo.page.defaultValue);
    History.performManualSearch();
  });

  return buttonDiv;
};

History.buildHiddenFields = function() {
  var hiddenDiv = $('<div>', { 'style': 'display: none; ' });

  $.each(History.searchParameterInfo, function(name, info) {
    if (info.inputType == 'hidden') {
      var hiddenInput = $('<input>', {
        'type': 'hidden',
        'id': 'parameter_' + name,
      });
      hiddenDiv.append(hiddenInput);

      if (History.searchParameters !== undefined &&
        History.searchParameters[name] !== undefined) {
        hiddenInput.val(History.searchParameters[name]);
      }
    }
  });

  return hiddenDiv;
};

History.buildResultsTableHeader = function() {
  var head = $('<thead>');

  var headerRow = $('<tr>');
  head.append(headerRow);
  var filterRow = $('<tr>');
  head.append(filterRow);

  $.each(History.searchParameterInfo, function(columnId, columnInfo) {
    if (columnInfo.inputType == 'hidden' || columnInfo.inputType == 'special') {
      return;
    }

    var titleTh = $('<th>');
    headerRow.append(titleTh);

    var sortButtonColor;
    if (History.searchParameters !== undefined &&
      History.searchParameters.sortColumn !== undefined &&
      History.searchParameters.sortDirection !== undefined &&
      History.searchParameters.sortColumn == columnId &&
      History.searchParameters.sortDirection == 'ASC') {
      sortButtonColor = '#ffffff';
    } else {
      sortButtonColor = '#000000';
    }
    titleTh.append($('<span>', {
      'html': '&#9650',
      'href': 'javascript: void(0);',
      'data-column': columnId,
      'data-direction': 'ASC',
      'class': 'sortButton',
      'style': 'color: ' + sortButtonColor + ';',
      'title': 'Sort by ' + columnInfo.text + ' in ascending order',
    }));

    titleTh.append($('<span>', {
      'text': columnInfo.text,
      'title': columnInfo.toolTip,
    }));

    if (History.searchParameters !== undefined &&
      History.searchParameters.sortColumn !== undefined &&
      History.searchParameters.sortDirection !== undefined &&
      History.searchParameters.sortColumn == columnId &&
      History.searchParameters.sortDirection == 'DESC') {
      sortButtonColor = '#ffffff';
    } else {
      sortButtonColor = '#000000';
    }
    titleTh.append($('<span>', {
      'html': '&#9660;',
      'href': 'javascript: void(0);',
      'data-column': columnId,
      'data-direction': 'DESC',
      'class': 'sortButton',
      'style': 'color: ' + sortButtonColor + ';',
      'title': 'Sort by ' + columnInfo.text + ' in descending order',
    }));

    var filterTd = $('<th>');
    filterRow.append(filterTd);

    switch (columnInfo.inputType) {
    case 'select':
      var selectList = $('<select>', {
        'name': columnId,
        'id': 'parameter_' + columnId,
        'title': columnInfo.toolTip,
      });
      filterTd.append(selectList);

      selectList.append($('<option>', {
        'value': '',
        'text': '',
      }));

      $.each(columnInfo.source, function (value, name) {
        selectList.append($('<option>', {
          'value': value,
          'text': name,
        }));
      });

      if (History.searchParameters !== undefined) {
        selectList.val(History.searchParameters[columnId]);
      }
      break;
    case 'dateRange':
      var range = { 'Min': 'Minimum', 'Max': 'Maximum' };
      $.each(range, function(abbrev, full) {
        var dateDiv = $('<div>');
        filterTd.append(dateDiv);

        var dateElement = $('<input>', {
          'type': 'text',
          'name': columnId + abbrev,
          'id': 'parameter_' + columnId + abbrev,
          'maxlength': '10',
          'class': 'dateInput',
          'title': full +  ' ' + columnInfo.text + ' Date',
        });
        dateDiv.append(dateElement);
        if (History.searchParameters !== undefined) {
          var date =
            Env.formatTimestamp(History.searchParameters[columnId + abbrev],
              'date');
          dateElement.val(date);
        }
        dateElement.datepicker({ 'dateFormat': 'yy-mm-dd', });

        var calendar = $('<img>', {
          'src': Env.ui_root + 'images/calendar.png',
          'class': 'calendar',
          'title': full +  ' ' + columnInfo.text + ' Date',
        });
        dateDiv.append(calendar);
        calendar.click(function() {
          dateElement.datepicker('show');
        });
      });
      break;
    default:
      var inputElement = $('<input>', {
        'type': columnInfo.inputType,
        'name': columnId,
        'id': 'parameter_' + columnId,
        'title': columnInfo.toolTip,
      });
      filterTd.append(inputElement);
      if (History.searchParameters !== undefined) {
        inputElement.val(History.searchParameters[columnId]);
      }
    }
  });

  head.find('.sortButton').click(function() {
    $('#parameter_sortColumn').val($(this).attr('data-column'));
    $('#parameter_sortDirection').val($(this).attr('data-direction'));
    History.performManualSearch();
  });

  return head;
};

History.buildResultsTableBody = function() {
  var body = $('<tbody>');

  if (Api.game_history === undefined ||
    Api.game_history.games === undefined)  {
    return body;
  }

  $.each(Api.game_history.games, function(index, game) {
    var gameRow = $('<tr>');
    body.append(gameRow);

    var $nextPlayerColor = '#ffffff';
    if (game.playerNameA == Login.player) {
      if (game.waitingOnA) {
        $nextPlayerColor = game.colorA;
      } else if (game.waitingOnB) {
        $nextPlayerColor = game.colorB;
      }
    } else if (game.playerNameB == Login.player) {
      if (game.waitingOnB) {
        $nextPlayerColor = game.colorB;
      } else if (game.waitingOnA) {
        $nextPlayerColor = game.colorA;
      }
    }

    var idTd = $('<td>', {
      'style': 'background-color: ' + $nextPlayerColor + ';',
    });
    idTd.append($('<a>', {
      'href': 'game.html?game=' + game.gameId,
      'text': 'Game ' + game.gameId,
    }));
    gameRow.append(idTd);

    gameRow.append($('<td>', {
      'style': 'background-color: ' + game.colorA + ';',
    }).append(Env.buildProfileLink(game.playerNameA)));
    gameRow.append($('<td>').append(
      Env.buildButtonLink(game.buttonNameA)
    ));
    gameRow.append($('<td>').append(
      Env.buildButtonLink(game.buttonNameB)
    ));
    gameRow.append($('<td>', {
      'style': 'background-color: ' + game.colorB + ';',
    }).append(Env.buildProfileLink(game.playerNameB)));
    gameRow.append($('<td>', {
      'text': Env.formatTimestamp(game.gameStart, 'date'),
    }));
    gameRow.append($('<td>', {
      'text': Env.formatTimestamp(game.lastMove, 'date'),
    }));

    gameRow.append(History.scoreCol(game));

    if (game.status == 'COMPLETE') {
      gameRow.append($('<td>', {
        'text': 'Completed',
        'style': 'font-weight: bold;'
      }));
    } else if (game.status == 'CANCELLED') {
      gameRow.append($('<td>', {
        'text': 'Cancelled',
        'style': 'color: #aaaaaa;'
      }));
    } else if ((game.status == 'NEW') || (game.status == 'OPEN')) {
      gameRow.append($('<td>', {
        'text': 'Unstarted',
        'style': 'color: #aaaaaa;'
      }));
    } else {
      gameRow.append($('<td>', {
        'text': 'In Progress',
        'style': 'font-style: italic;'
      }));
    }
  });

  return body;
};

History.scoreCol = function(game) {
  var score;
  if ((game.status == 'CANCELLED') ||
      (game.status == 'NEW') ||
      (game.status == 'OPEN')) {
    score = '–/–/–';
  } else {
    score = game.roundsWonA + '/' + game.roundsWonB + '/' + game.roundsDrawn;
  }
  score += ' (' + game.targetWins + ')';

  var winnerColor;
  if (game.roundsWonA >= game.targetWins) {
    winnerColor = game.colorA;
  } else if (game.roundsWonB >= game.targetWins) {
    winnerColor = game.colorB;
  } else {
    winnerColor = '#ffffff';
  }

  var column = $('<td>', {
    'text': score,
    'style': 'background-color: ' + winnerColor + ';',
  });

  return column;
};

History.buildResultsTableFooter = function() {
  var foot = $('<tfoot>');

  if (Api.game_history === undefined ||
    Api.game_history.summary === undefined)  {
    return foot;
  }

  var summary = Api.game_history.summary;
  var lastPage =
    Math.ceil(summary.matchesFound / History.searchParameters.numberOfResults);

  var footerHeaderRow = $('<tr>');
  foot.append(footerHeaderRow);

  var matchesToolTip;
  if (summary.matchesFound == 1) {
    matchesToolTip = 'There is 1 matching game';
  } else {
    matchesToolTip = 'There are ' + summary.matchesFound + ' matching games';
  }
  footerHeaderRow.append($('<th>', {
    'text': 'Matches Found',
    'title': matchesToolTip,
  }));

  var pagesToolTip;
  if (lastPage == 1) {
    pagesToolTip = 'There is 1 page of matching games';
  } else {
    pagesToolTip = 'There are ' + lastPage + ' pages of matching games';
  }
  footerHeaderRow.append($('<th>', {
    'text': 'Pages',
    'colspan': '4',
    'style': 'text-align: left;',
    'title': pagesToolTip,
  }));
  footerHeaderRow.append($('<th>', {
    'text': 'Earliest Start',
    'title': 'The earliest Game Start date for any matching game',
  }));
  footerHeaderRow.append($('<th>', {
    'text': 'Latest Move',
    'title': 'The latest Last Move date for any matching game',
  }));
  footerHeaderRow.append($('<th>', {
    'text': 'Games W/L/I',
    'title': 'Won by Player A / Lost by Player A / Incomplete',
  }));
  footerHeaderRow.append($('<th>', {
    'text': '% Completed',
    'title': 'What fraction of the matching games have been completed so far',
  }));

  var footerDataRow = $('<tr>');
  foot.append(footerDataRow);

  footerDataRow.append($('<td>', { 'text': summary.matchesFound }));

  var pagingTd = $('<td>', {
    'colspan': '4',
    'style': 'text-align: left;',
  });

  if (History.searchParameters.page > 1) {
    pagingTd.append($('<span>', {
      'html': '&larr;',
      'class': 'pageLink',
      'style': 'width: 1.2em;',
      'data-page': parseInt(History.searchParameters.page, 10) - 1,
    }));
  } else {
    // We'll add a blank pager so that the paging links stay lined up right
    // whether we're on page one or not
    pagingTd.append($('<span>', {
      'class': 'pageLink',
      'style': 'width: 1.2em;',
      'data-page': History.searchParameters.page,
    }));
  }

  for (var i = 1; i <= lastPage; i++) {
    if (i == History.searchParameters.page) {
      pagingTd.append($('<span>', {
        'text': '[' + i + ']',
        'class': 'pageLink',
        'data-page': i,
        'style': 'font-weight: bold; cursor: default;'
      }));
    } else {
      pagingTd.append($('<span>', {
        'text': i,
        'class': 'pageLink',
        'data-page': i,
      }));
    }
  }

  if (History.searchParameters.page < lastPage) {
    pagingTd.append($('<span>', {
      'html': '&rarr;',
      'class': 'pageLink',
      'style': 'width: 1.2em;',
      'data-page': parseInt(History.searchParameters.page, 10) + 1,
    }));
  }

  pagingTd.find('.pageLink').click(function() {
    var page = $(this).attr('data-page');
    if (page != History.searchParameters.page) {
      $('#parameter_page').val(page);
      History.performManualSearch();
    }
  });
  footerDataRow.append(pagingTd);

  footerDataRow.append($('<td>', {
    'text': Env.formatTimestamp(summary.earliestStart, 'date'),
    'title': 'The earliest Game Start date for any matching game',
  }));
  footerDataRow.append($('<td>', {
    'text': Env.formatTimestamp(summary.latestMove, 'date'),
    'title': 'The latest Last Move date for any matching game',
  }));

  var scores =
    summary.gamesWonA + '/' + summary.gamesWonB + '/' +
    (summary.matchesFound - summary.gamesCompleted);
  footerDataRow.append($('<td>', {
    'text': scores,
    'title': 'Won by Player A / Lost by Player A / Incomplete',
  }));

  var percentCompleted = '';
  if (summary.matchesFound > 0) {
    percentCompleted = (summary.gamesCompleted * 100) / summary.matchesFound;
    percentCompleted = Math.round(percentCompleted) + '%';
  }
  footerDataRow.append($('<td>', {
    'text': percentCompleted,
    'title': 'What fraction of the matching games have been completed so far',
  }));

  return foot;
};


<|start_filename|>test/src/ui/js/test_Verify.js<|end_filename|>
module("Verify", {
  'setup': function() {
    BMTestUtils.VerifyPre = BMTestUtils.getAllElements();

    // Override Env.getParameterByName to get verification parameters
    BMTestUtils.overrideGetParameterByName();

    // Create the verify_page div so functions have something to modify
    if (document.getElementById('verify_page') == null) {
      $('body').append($('<div>', {'id': 'env_message', }));
      $('body').append($('<div>', {'id': 'verify_page', }));
    }
  },
  'teardown': function(assert) {

    // Do not ignore intermittent failures in this test --- you
    // risk breaking the entire suite in hard-to-debug ways
    assert.equal(jQuery.active, 0,
      "All test functions MUST complete jQuery activity before exiting");

    // Delete all elements we expect this module to create

    // JS objects
    delete Verify.page;

    // Page elements
    $('#verify_page').remove();

    BMTestUtils.deleteEnvMessage();
    BMTestUtils.restoreGetParameterByName();

    BMTestUtils.restoreGetParameterByName();

    // Fail if any other elements were added or removed
    BMTestUtils.VerifyPost = BMTestUtils.getAllElements();
    assert.deepEqual(
      BMTestUtils.VerifyPost, BMTestUtils.VerifyPre,
      "After testing, the page should have no unexpected element changes");
  }
});

// pre-flight test of whether the Verify module has been loaded
test("test_Verify_is_loaded", function(assert) {
  assert.ok(Verify, "The Verify namespace exists");
});

// The purpose of this test is to demonstrate that the flow of
// Verify.showLoggedInPage() is correct for a showXPage function,
// namely that it calls an API getter with a showStatePage function
// as a callback.
//
// Accomplish this by mocking the invoked functions
test("test_Verify.showLoggedInPage", function(assert) {
  expect(5);
  var cached_getVerifyParams = Verify.getVerifyParams;
  var cached_showStatePage = Verify.showStatePage;
  var getVerifyParamsCalled = false;
  Verify.showStatePage = function() {
    assert.ok(getVerifyParamsCalled, "Verify.getVerifyParams is called before Verify.showStatePage");
  };
  Verify.getVerifyParams = function(callback) {
    getVerifyParamsCalled = true;
    assert.equal(callback, Verify.showStatePage,
      "Verify.getVerifyParams is called with Verify.showStatePage as an argument");
    callback();
  };

  Verify.showLoggedInPage();
  var item = document.getElementById('verify_page');
  assert.equal(item.nodeName, "DIV",
        "#verify_page is a div after showLoggedInPage() is called");
  Verify.getVerifyParams = cached_getVerifyParams;
  Verify.showStatePage = cached_showStatePage;
});

// At this point, Verify.showLoggedOutPage is a pointer to
// Verify.showLoggedInPage
test("test_Verify.showLoggedOutPage", function(assert) {
  assert.equal(Verify.showLoggedOutPage, Verify.showLoggedInPage,
    "Verify.showLoggedOutPage and Verify.showLoggedInPage should be the same");
});

test("test_Verify.getVerifyParams", function(assert) {
  stop();
  Verify.getVerifyParams(function() {
    assert.equal(Env.message.type, "success",
          "getVerifyParams() succeeds in its POST");
    start();
  });
});

test("test_Verify.showStatePage", function(assert) {
// FIXME: put a test here that actually meaningfully tests the code
});

test("test_Verify.setVerifyUserSuccessMessage", function(assert) {
  Verify.setVerifyUserSuccessMessage('test success');
  assert.equal(Env.message.type, 'success', 'message type is success');
});

test("test_Verify.setVerifyUserFailureMessage", function(assert) {
  Verify.setVerifyUserFailureMessage('test failure');
  assert.equal(Env.message.type, 'error', 'message type is error');
});


<|start_filename|>notes/database/Tournament List.html<|end_filename|>
<HTML>
<HEAD>
  <!-- Created by GNNpress/1.2 -->
  <TITLE>Button Men: Tournament List</TITLE>
</HEAD>
<BODY BGCOLOR="#ffffff" TEXT="#000000" LINK="#5887a1" VLINK="#a16abd">
<P ALIGN=Center>
<FONT COLOR="White"><FONT size=6><B><A HREF="thelist.html">Back to The
List</A></B></FONT></FONT>&nbsp;
<P ALIGN=Center>
<IMG BORDER="0" ALIGN="Middle" SRC="barely.jpg" ALT="Barely Legal" WIDTH="173"
    HEIGHT="136">
<P ALIGN=Center>
<FONT COLOR="Black"><FONT size=6><B>The beatpeopleup.com Tournament
List</B></FONT></FONT>
<P ALIGN=Left>
Here is a brief (and easliy printable!) list of every licensed Button Man
in the universe, and whether that character is legal in tournaments. This
list is changing all the time, and the current draft is always available
at www.beatpeopleup.com.
<P ALIGN=Left>
Please be aware that every tournament is different, and in fact we encourage
you to run tournaments with different lists than the one given here. This
is only meant as a jumping-off point, a list of those buttons we think are
fair enough to play with.
<P ALIGN=Left>
If you have suggestions for why certain Button Men should be on or off this
list, we want to hear them. Basically, we want to make sure that people who
want a fair game can get one!
<P ALIGN=Left>
In roughly chronological order, the Tournament Legal Button Men are:
<P ALIGN=Left>
  <HR>
<P ALIGN=Left>
Button Men Soldiers: All 12 commons, but not Changeling. This includes Avis,
Hammer, Bauer, Stark, Clare, Kith, Karl, Iago, Niles, Shore, Hannah, and
Kublai.
<P ALIGN=Left>
Sanctum: All 4: Dracha, Ogi, Fingle, and Ngozi.
<P ALIGN=Left>
Lunch Money: All 7, including the rare. This includes Charity, Prudence,
Hope, Chastity, Faith, Temperance, and Patience.
<P ALIGN=Left>
Button Men Vampires: All 6: Angel, Buddy, Dunkirk, Starchylde, McGinty, and
Tiffany.
<P ALIGN=Left>
Legend of the Five Rings: All 12: Crab, Crane, Dragon, Lion, Mantis, Naga,
Phoenix, Ronin, Scorpion, Unicorn, Mirumoto, and Kakita.
<P ALIGN=Left> Button Men BROM: All 14, including the Rares. This includes Coil, 
  Bane, Lucky, Shepherd, Peace, Crusher, Grist, Wastenott, Reaver, Jellybean, 
  Bluff, Strik, Echo, and Giant. (Echo and Giant need special care.) 
<P ALIGN=Left>
Misc. 1999 Buttons: Bunnies and Lab Rat, no. Brigid and O-Lass: yes. Zeppo:
Yes. Eiko and Wu Lan: yes. Buzzing Weasel: yes. <NAME>: heck, no. Dirgo
and Flaire: yes. Poly and Adam Spam: yes. Fuzzface: yes.
<P ALIGN=Left>
Button Men BRAWL: All 6 commons, but not Sydney. This includes Bennett, Chris,
Darwin, Hale, Morgan, and Pearl.
<P ALIGN=Left>
Buttonlords: All 8: Arthur, Mordred, Lancelot, Gawaine, Morgan, Guenever,
Nimue, and Merlin.
<P ALIGN=Left>
Studio Foglio: All except Winslow and the rare, James. This includes Phil,
Dixie, Growf, Buck,, Brigid, O-Lass, Agatha, Krosp, Klaus, <NAME>, Bang,
and Gil
<P ALIGN=Left>
Sailor Moon 1: All 12. Sailors Moon, Mercury, Mars, Jupiter, and Venus, Tuxedo
Mask, Queen Beryl, Zoycite, Jedite, Nephlite, Malachite, and Shadow Warriors.
<P ALIGN=Left>
Button Men Freaks: All 4. Max, Mister Peach, Simon, and Werner.
<P ALIGN=Left>
Button Men Fantasy: All 14, including rares. This includes Luce, Frasquito,
Lark, Theophilus, Mischa, Chang, Aldric, Elihu, Farrell, Nikolai, Cesare,
Vincent, Nightmare, and Socrates.
<P ALIGN=Left>
Button Men Dork Victory: All 6. Bill, Carson, Gilly, Igor, Ken, and Matt.
<P ALIGN=Left>
Sailor Moon 2: All 12. Luna / Artemis, Wicked Lady, Queen Serenity, Rubeus,
Princess Serena, Sapphire, Rini, Wiseman, Neo-Queen Serenity, Prince Diamond,
King Endymion, and Emerald.
<P ALIGN=Left>
Bruno: All 4. Bruno, Pappy, Synthia, and the GM.
<P ALIGN=Left>
Misc. 2000 Buttons: Gordo: no. Social Class: yes. Tirade: no. Tess: yes.
Guillermo: yes. Ginzu and Gratch: yes. Bush, Gore, and Cthulhu: yes. Me am
ORK: yes. Nerni and Yeti: yes. Sven and Yseulte: yes. Japanese Beetle and
Flying Squirrel: yes. Howling Wolf and White Tiger: yes.
<P ALIGN=Left>
Sluggy Freelance: All 6. Aylee, Bun-Bun, Kiki, Riff, Torg, and Zo&euml;.
<P ALIGN=Left>
Samurai: All seven, including the rare. Honzo, Konami, Okaru, Sadakuro, Tonase,
Yuranosuke, and Tsusuko.
<P ALIGN=Left>
Misc. 2001 Buttons: Catfight Girls: yes. Jorgi: yes. The Effster and the
Fictitious Alan Clark: yes.
<P ALIGN=Center>
  <HR>
<P ALIGN=Center>
Beat People Up, the Button Men and BRAWL Home Page, is (c) 2002
<A HREF="mailto:&#104;&#105;&#109;s&#101;&#108;&#102;&#64;c&#104;&#101;&#97;p&#97;&#115;s.&#99;&#111;m"><NAME></A> and Cheapass Games.
<P ALIGN=Center>
<A HREF="index.html"><IMG BORDER="0" ALIGN="Middle" SRC="btnhome.jpg" ALT="Home"
    WIDTH="70" HEIGHT="54"></A>
</BODY></HTML>


<|start_filename|>test/src/ui/js/test_UserPrefs.js<|end_filename|>
module("UserPrefs", {
  'setup': function() {
    BMTestUtils.UserPrefsPre = BMTestUtils.getAllElements();
    BMTestUtils.setupFakeLogin();

    // Create the userprefs_page div so functions have something to modify
    if (document.getElementById('userprefs_page') == null) {
      $('body').append($('<div>', {'id': 'env_message', }));
      $('body').append($('<div>', {'id': 'userprefs_page', }));
    }

    Login.pageModule = { 'bodyDivId': 'userprefs_page' };
  },
  'teardown': function(assert) {

    // Do not ignore intermittent failures in this test --- you
    // risk breaking the entire suite in hard-to-debug ways
    assert.equal(jQuery.active, 0,
      "All test functions MUST complete jQuery activity before exiting");

    // Delete all elements we expect this module to create

    // JS objects
    delete Api.button;
    delete Api.player;
    delete Api.user_prefs;
    delete UserPrefs.page;
    delete UserPrefs.form;

    Login.pageModule = null;

    // Page elements
    $('#userprefs_page').remove();
    // Controls added to the page by the color picker library we use
    $('.sp-container').remove();

    BMTestUtils.deleteEnvMessage();
    BMTestUtils.cleanupFakeLogin();

    // Fail if any other elements were added or removed
    BMTestUtils.UserPrefsPost = BMTestUtils.getAllElements();
    assert.deepEqual(
      BMTestUtils.UserPrefsPost, BMTestUtils.UserPrefsPre,
      "After testing, the page should have no unexpected element changes");
  }
});

// pre-flight test of whether the UserPrefs module has been loaded
test("test_UserPrefs_is_loaded", function(assert) {
  assert.ok(UserPrefs, "The UserPrefs namespace exists");
});

// The purpose of this test is to demonstrate that the flow of
// UserPrefs.showLoggedInPage() is correct for a showXPage function, namely
// that it calls an API getter with a showStatePage function as a
// callback.
//
// Accomplish this by mocking the invoked functions
test("test_UserPrefs.showLoggedInPage", function(assert) {
  expect(5);
  var cached_getter = Env.callAsyncInParallel;
  var cached_showStatePage = UserPrefs.assemblePage;
  var getterCalled = false;
  UserPrefs.assemblePage = function() {
    assert.ok(getterCalled, "Env.callAsyncInParallel is called before UserPrefs.assemblePage");
  };
  Env.callAsyncInParallel = function(scripts, callback) {
    getterCalled = true;
    assert.equal(callback, UserPrefs.assemblePage,
      "Env.callAsyncInParallel is called with UserPrefs.assemblePage as an argument");
    callback();
  };

  UserPrefs.showLoggedInPage();
  var item = document.getElementById('userprefs_page');
  assert.equal(item.nodeName, "DIV",
        "#userprefs_page is a div after showLoggedInPage() is called");

  Env.callAsyncInParallel = cached_getter;
  UserPrefs.assemblePage = cached_showStatePage;
});

test("test_UserPrefs.assemblePage", function(assert) {
  stop();
  Env.callAsyncInParallel([
    { 'func': Api.getButtonData, 'args': [ null ] },
    Api.getUserPrefsData,
  ], function() {
    UserPrefs.assemblePage();
    var htmlout = UserPrefs.page.html();
    assert.ok(htmlout.length > 0,
       "The created page should have nonzero contents");
    start();
  });
});

test("test_UserPrefs.actionFailed", function(assert) {
  UserPrefs.actionFailed();
  assert.equal(UserPrefs.form, null, "The failing action does not set a form");
});

test("test_UserPrefs.actionSetPrefs", function(assert) {
  stop();
  Env.callAsyncInParallel([
    { 'func': Api.getButtonData, 'args': [ null ] },
    Api.getUserPrefsData,
  ], function() {
    UserPrefs.actionSetPrefs();
    var autopass_checked = $('#userprefs_autopass').prop('checked');
    assert.ok(autopass_checked,
       "The autopass button should be checked in the prefs table");
    start();
  });
});

test("test_UserPrefs.actionSetPrefsFireOvershooting", function(assert) {
  stop();
  Env.callAsyncInParallel([
    { 'func': Api.getButtonData, 'args': [ null ] },
    Api.getUserPrefsData,
  ], function() {
    UserPrefs.actionSetPrefs();
    var fire_overshooting_checked = $('#userprefs_fire_overshooting').prop('checked');
    assert.ok(!fire_overshooting_checked,
       "The fire overshooting button should not be checked in the prefs table");
    start();
  });
});

// The logic here is a little hairy: since Api.getUserPrefsData()
// takes a callback, we can use the normal asynchronous logic there.
// However, the POST done by our forms doesn't take a callback (it
// just redraws the page), so turn off asynchronous handling in
// AJAX while we test that, to make sure the test sees the return
// from the POST.
test("test_UserPrefs.formSetPrefs", function(assert) {
  stop();
  Env.callAsyncInParallel([
    { 'func': Api.getButtonData, 'args': [ null ] },
    Api.getUserPrefsData,
  ], function() {
    UserPrefs.actionSetPrefs();
    $.ajaxSetup({ async: false });
    $('#userprefs_action_button').trigger('click');
    assert.deepEqual(
      Env.message,
      {"type": "success", "text": "User details set successfully."},
      "User preferences save succeeded");
    $.ajaxSetup({ async: true });
    start();
  });
});

test("test_UserPrefs.appendToPreferencesTable", function(assert) {
  var table = $('<table>');
  var prefs = {
    'testing' : {
      'text': 'Testing',
      'type': 'checkbox',
      'checked': true,
    },
  };

  UserPrefs.appendToPreferencesTable(table, 'Test Preferences',
    'These are not real. There is no spoon.', prefs);
  var checkbox = table.find('input#userprefs_testing');

  assert.ok(checkbox.val(), 'User preference control created and populated');
});


<|start_filename|>src/ui/js/Overview.js<|end_filename|>
// namespace for this "module"
var Overview = {};

Overview.bodyDivId = 'overview_page';

Overview.MONITOR_TIMEOUT = 60;
Overview.STALENESS_DAYS = 14;
Overview.STALENESS_SECS = Overview.STALENESS_DAYS * 24 * 60 * 60;

////////////////////////////////////////////////////////////////////////
// Action flow through this page:
// * Overview.showLoggedInPage() is the landing function.  Always call this
//   first when logged in. This will either call Login.goToNextPendingGame,
//   Overview.executeMonitor() or Overview.getOverview() and
//   Overview.showPage() (depending on the "mode" parameter).
// * Overview.showLoggedInPage() is the landing function.  Always call this
//   first when logged out. This calls Overview.pageAddIntroText()
// * Overview.getOverview() asks the API for information about the
//   player's overview status (currently, the lists of new, active, completed,
//   and cancelled games, and potentially the user's preferences).
//   It sets Api.new_games, Api.active_games, Api.completed_games,
//   Api.cancelled_games, and potentially Api.user_prefs. If successful, it
//   calls Overview.showPage().
// * Overview.showPage() assembles the page contents as a variable.
//
// N.B. There is no form submission on this page (aside from the [Dismiss]
// links); it's just a landing page with links to other pages. So it's
// logically somewhat simpler than e.g. Game.js.
////////////////////////////////////////////////////////////////////////

Overview.showLoggedInPage = function() {
  // Set up the callback for refreshing the page if there's no next game
  Login.nextGameRefreshCallback = Overview.showPreferredOverview;

  var mode = Env.getParameterByName('mode');

  switch (mode) {
  case 'nextGame':
    // Try to go to the next game
    Api.getNextGameId(Login.goToNextPendingGame);
    break;
  case 'monitor':
    Overview.monitorIsOn = true;
    // If we're in monitor mode, run the monitor first
    Api.getUserPrefsData(function() {
      Overview.executeMonitor();
    });
    break;
  case 'preference':
    Overview.showPreferredOverview();
    break;
  default:
    Overview.monitorIsOn = false;
    // Get all needed information, then display overview page
    Overview.getOverview(Overview.showPage);
    break;
  }
};

Overview.showLoggedOutPage = function() {
  Overview.page = $('<div>');
  Overview.pageAddIntroText();
  // Actually lay out the page
  Login.arrangePage(Overview.page);
};

Overview.showPreferredOverview = function() {
  Api.getUserPrefsData(function() {
    if (Api.user_prefs.automatically_monitor) {
      Overview.monitorIsOn = true;
      // If we're in monitor mode, run the monitor first
      Overview.executeMonitor();
    } else {
      Overview.monitorIsOn = false;
      // Get all needed information, then display overview page
      Overview.getOverview(Overview.showPage);
    }
  });
};

Overview.getOverview = function(callback) {
  Env.callAsyncInParallel([
    Api.getNewGamesData,
    Api.getActiveGamesData,
    Api.getCompletedGamesData,
    Api.getCancelledGamesData,
  ], callback);
};

Overview.showPage = function() {
  Overview.page = $('<div>');

  var nGamesAwaitingAction = Api.new_games.nGamesAwaitingAction +
    Api.active_games.nGamesAwaitingAction;
  var gameCountText='';
  if (nGamesAwaitingAction > 0) {
    gameCountText = '(' + nGamesAwaitingAction+ ') ';
  }
  $('title').html(gameCountText + 'Button Men Online');

  Overview.pageAddNewgameLink();

  if (Overview.monitorIsOn) {
    Overview.page.append($('<h2>', {
      'text': '* Monitor Active *',
      'class': 'monitorMessage',
    }));
    // Convert milliseconds (javascript-style) to seconds (unix-style)
    var currentTimestamp = new Date().getTime() / 1000;
    Overview.page.append($('<div>', {
      'text': 'Last refresh: ' + Env.formatTimestamp(currentTimestamp),
      'class': 'monitorTimestamp',
    }));
    Overview.page.append($('<div>').append($('<a>', {
      'text': 'Disable Monitor',
      'href': Env.ui_root,
    })));

    // Times 1000 because setTimeout expects milliseconds
    setTimeout(Overview.executeMonitor, Overview.MONITOR_TIMEOUT * 1000);
  }

  if ((Api.new_games.nGames === 0) &&
      (Api.active_games.nGames === 0) &&
      (Api.completed_games.nGames === 0) &&
      (Api.cancelled_games.nGames === 0)) {
    Env.message = {
      'type': 'none',
      'text': 'You have no games',
    };
  } else {
    Overview.pageAddGameTables();
  }

  // Actually lay out the page
  Login.arrangePage(Overview.page);

  Overview.updateSectionHeaderCounts('activeGames');
  Overview.updateSectionHeaderCounts('closedGames');
};

Overview.updateSectionHeaderCounts = function(tableClass) {
  var header = Overview.page.find(
    'div.' + tableClass + 'Holder h2.sectionHeader'
  );
  var tableRows = Overview.page.find(
    'div.' + tableClass +
    'Holder table.gameList tbody tr:visible:not(.spacer)'
  );
  var count = tableRows.size();

  header.text(header.attr('data-text') + ' (' + count + ')');
};

Overview.executeMonitor = function() {
  Api.automatedApiCall = true;
  if (Api.user_prefs.monitor_redirects_to_game &&
      Api.user_prefs.monitor_redirects_to_forum) {
    Env.callAsyncInParallel([
      Api.getNextGameId,
      Api.getNextNewPostId,
    ], Overview.completeMonitor);
  } else if (Api.user_prefs.monitor_redirects_to_game) {
    Api.getNextGameId(Overview.completeMonitor);
  } else if (Api.user_prefs.monitor_redirects_to_forum) {
    Api.getNextNewPostId(Overview.completeMonitor);
  } else {
    Overview.getOverview(Overview.showPage);
  }
};

Overview.completeMonitor = function() {
  if (Api.user_prefs.monitor_redirects_to_game &&
      Api.gameNavigation !== undefined && Api.gameNavigation.nextGameId) {
    Login.goToNextPendingGame();
    return;
  }
  if (Api.user_prefs.monitor_redirects_to_forum &&
      Api.forumNavigation !== undefined && Api.forumNavigation.nextNewPostId) {
    Overview.goToNextNewForumPost();
    return;
  }

  Overview.getOverview(Overview.showPage);
};

////////////////////////////////////////////////////////////////////////
// Helper routines to add HTML entities to existing pages

// Add tables for types of existing games
Overview.pageAddGameTables = function() {
  Overview.pageAddGameTable('closed', 'Closed games', false);
  Overview.pageAddGameTable('awaitingPlayer', 'Active games', false);
  Overview.pageAddGameTable('awaitingOpponent', 'Active games', true);
};

Overview.pageAddNewgameLink = function() {
  var newgameDiv = $('<div>');
  var newgamePar = $('<p>');
  if (Api.active_games.games.awaitingPlayer.length > 0) {
    var newgameLink = $('<a>', {
      'href': Env.ui_root + 'index.html?mode=nextGame',
      'text': 'Go to your next pending game',
    });
    newgamePar.append(newgameLink);
    newgameLink.click(function(e) {
      e.preventDefault();
      Api.getNextGameId(Login.goToNextPendingGame);
    });
  } else if (Api.active_games.games.awaitingOpponent.length > 0) {
    // just return in this case, and don't add a message at all
    return;

  } else {
    newgamePar.append($('<a>', {
      'href': 'create_game.html',
      'text': 'Create a new game',
    }));
    newgamePar.append(' or ');
    newgamePar.append($('<a>', {
      'href': 'open_games.html',
      'text': 'join an open game',
    }));
  }
  newgameDiv.append(newgamePar);
  Overview.page.append(newgameDiv);
};

Overview.pageAddGameTable = function(
    gameType,
    sectionHeader,
    reverseSortOrder
  ) {
  var gamesource;
  var tableClass;
  var showDismiss = false;

  switch (gameType) {
  case 'closed':
    // closed games comprise completed games and cancelled games
    var gamesourceCompleted = Api.completed_games.games;
    Overview.addTypeToGameSource(gamesourceCompleted, 'completed');

    var gamesourceCancelled = Api.cancelled_games.games;
    Overview.addTypeToGameSource(gamesourceCancelled, 'cancelled');

    gamesource = gamesourceCompleted.concat(gamesourceCancelled);
    gamesource.sort(function(a, b) {
      return a.gameId - b.gameId;
    });

    tableClass = 'closedGames';
    showDismiss = true;
    break;
  default:
    // show both active and new games under 'Active' games
    var gamesourceActive = Api.active_games.games[gameType];
    Overview.addTypeToGameSource(gamesourceActive, 'active');

    var gamesourceNew = Api.new_games.games[gameType];
    Overview.addTypeToGameSource(gamesourceNew, 'new');

    gamesource = gamesourceActive.concat(gamesourceNew);
    gamesource.sort(function(a, b) {
      return b.inactivityRaw - a.inactivityRaw;
    });
    tableClass = 'activeGames';
  }

  if (gamesource.length === 0) {
    return;
  }

  if (reverseSortOrder === true) {
    gamesource.reverse();
  }

  Overview.addTableRows(
    Overview.addTableStructure(tableClass, sectionHeader, showDismiss),
    gamesource,
    gameType,
    showDismiss
  );
};

Overview.addTypeToGameSource = function(gamesource, gameType) {
  for (var gameIdx = 0; gameIdx < gamesource.length; gameIdx++) {
    gamesource[gameIdx].gameType = gameType;
  }
};

Overview.addTableStructure = function(tableClass, sectionHeader, showDismiss) {
  var tableBody = Overview.page.find('table.' + tableClass + ' tbody');

  if (tableBody.length === 0) {
    // create table
    var tableDiv = $('<div>', { 'class': tableClass + 'Holder', });
    tableDiv.append($('<h2>', {
      'text': sectionHeader,
      'class': 'sectionHeader',
      'data-text': sectionHeader,
    }));
    var table = $('<table>', { 'class': 'gameList ' + tableClass, });
    tableDiv.append(table);
    Overview.page.append(tableDiv);

    // add table header
    var tableHead = $('<thead>');
    var headerRow = $('<tr>');
    headerRow.append($('<th>', {'text': 'Game', }));
    headerRow.append($('<th>', {'html': 'Your Button', }));
    headerRow.append($('<th>', {'html': 'Opponent\'s Button', }));
    headerRow.append($('<th>', {'text': 'Opponent', }));
    headerRow.append( $('<th>', {'html':
      'Score<br/>W/L/T (Max)',
    }));
    headerRow.append($('<th>', {'text': 'Description', }));
    headerRow.append($('<th>', {'text': 'Inactivity', }));
    if (showDismiss) {
      headerRow.append($('<th>', {'text': 'Dismiss', }));
    }
    tableHead.append(headerRow);
    table.append(tableHead);

    // add table body
    tableBody = $('<tbody>');
    table.append(tableBody);
  } else {
    // add spacer row
    var spacerRow = $('<tr>', { 'class': 'spacer' });
    tableBody.append(spacerRow);
    spacerRow.append($('<td>', {
      'html': '&nbsp;',
      'colspan': tableBody.closest('table')
                          .children('thead')
                          .children('tr:first')
                          .children()
                          .length,
    }));
  }

  return tableBody;
};

Overview.addTableRows = function(tableBody, gamesource, gameType, showDismiss) {
  var gameInfo;
  var gameRow;

  for (var gameIdx = 0; gameIdx < gamesource.length; gameIdx++) {
    gameInfo = gamesource[gameIdx];

    gameRow = $('<tr>');
    Overview.addGameCol(gameRow, gameInfo, gameType);
    Overview.addButtonCol(gameRow, gameInfo.playerButtonName);
    Overview.addButtonCol(gameRow, gameInfo.opponentButtonName);
    Overview.addPlayerCol(
      gameRow,
      gameInfo.opponentName,
      gameInfo.isOpponentOnVacation,
      gameInfo.opponentColor
    );
    Overview.addScoreCol(gameRow, gameInfo);
    Overview.addDescCol(gameRow, gameInfo.gameDescription);
    Overview.addInactiveCol(gameRow, gameInfo.inactivity);
    if (showDismiss) {
      Overview.addDismissCol(gameRow, gameInfo);
    }

    tableBody.append(gameRow);
  }

  if (tableBody.children('tr.staleGame').length > 0) {
    var nCol = gameRow.children().length;
    tableBody.closest('table').append(Overview.staleGameFooter(nCol));
  }
};

Overview.addGameCol = function(gameRow, gameInfo, gameType) {
  var gameLinkTd;

  if (gameType == 'awaitingPlayer') {
    gameLinkTd = $('<td>', {
      'style': 'background-color: ' + gameInfo.playerColor,
    });
  } else if (gameType == 'awaitingOpponent') {
    if (gameInfo.inactivityRaw > Overview.STALENESS_SECS) {
      gameRow.addClass('staleGame');
      gameRow.hide();
    }

    gameLinkTd = $('<td>', {
      'style': 'background-color: ' + gameInfo.opponentColor,
    });
  } else {
    gameLinkTd = $('<td>');
  }

  gameLinkTd.append($('<a>', {
    'href': 'game.html?game=' + gameInfo.gameId,
    'text': Overview.linkTextStub(gameInfo, gameType) +
            ' ' + gameInfo.gameId,
  }));

  gameRow.append(gameLinkTd);
};

Overview.addButtonCol = function(gameRow, buttonName) {
  gameRow.append($('<td>').append(
    Env.buildButtonLink(buttonName)
  ));
};

Overview.addPlayerCol = function(gameRow, playerName,
                                 isPlayerOnVacation, playerColor) {
  gameRow.append($('<td>', {
      'style': 'background-color: ' + playerColor,
    }).append((isPlayerOnVacation) ? Env.buildVacationImage() : '')
      .append(Env.buildProfileLink(playerName)));
};

Overview.addScoreCol = function(gameRow, gameInfo) {
  var wldColor = '#ffffff';
  if (gameInfo.gameScoreDict.W > gameInfo.gameScoreDict.L) {
    wldColor = gameInfo.playerColor;
  } else if (gameInfo.gameScoreDict.W < gameInfo.gameScoreDict.L) {
    wldColor = gameInfo.opponentColor;
  }

  if ((gameInfo.gameType == 'cancelled') ||
      (gameInfo.gameType == 'new')) {
    gameRow.append($('<td>', {
      'text': '–/–/–' + ' (' + gameInfo.maxWins + ')',
    }));
  } else {
    gameRow.append($('<td>', {
      'text': gameInfo.gameScoreDict.W + '/' +
              gameInfo.gameScoreDict.L + '/' +
              gameInfo.gameScoreDict.D + ' (' + gameInfo.maxWins + ')',
      'style': 'background-color: ' + wldColor,
    }));
  }
};

Overview.addDescCol = function(gameRow, description) {
  var descText = '';
  if (typeof(description) == 'string') {
    descText = description.substring(0, 30) +
               ((description.length > 30) ? '...' : '');
  }
  gameRow.append($('<td>', {
    'class': 'gameDescDisplay',
    'text': descText,
  }));
};

Overview.addInactiveCol = function(gameRow, inactivity) {
  gameRow.append($('<td>', { 'text': inactivity, }));
};

Overview.addDismissCol = function(gameRow, gameInfo) {
  var dismissTd = $('<td>');
  dismissTd.css('white-space', 'nowrap');
  gameRow.append(dismissTd);

  var dismissLink = $('<a>', {
    'text': 'Dismiss',
    'href': '#',
    'data-gameId': gameInfo.gameId,
  });
  dismissLink.click(Overview.formDismissGame);
  dismissTd.append('[')
           .append(dismissLink)
           .append(']');
};

Overview.linkTextStub = function(gameInfo, gameType) {
  if (gameInfo.gameType == 'new') {
    return 'NEW';
  } else if (gameInfo.gameType == 'cancelled') {
    return 'CANCELLED';
  } else if (gameInfo.gameType == 'completed') {
    if (gameInfo.gameScoreDict.W > gameInfo.gameScoreDict.L) {
      return 'WON';
    } else if (gameInfo.gameScoreDict.W < gameInfo.gameScoreDict.L) {
      return 'LOST';
    } else {
      return 'TIED';
    }
  } else if (gameType == 'awaitingPlayer') {
    return 'Play';
  } else if (gameType == 'awaitingOpponent') {
    return 'View';
  }
};

Overview.staleGameFooter = function(nCol) {
  var tableFoot = $('<tfoot>');
  var footRow = $('<tr>');
  var footCol = $('<td>', {'colspan': nCol,});
  var staleToggle = $('<a>', {
    'id': 'staleToggle',
    'href': 'javascript:Overview.toggleStaleGame();',
    'text': 'Show stale games',
  });

  footCol.append('[').append(staleToggle).append(']');
  footRow.append(footCol);
  tableFoot.append(footRow);

  return tableFoot;
};

Overview.toggleStaleGame = function() {
  $('.staleGame').toggle();
  $('#staleToggle').text(
    $('.staleGame').is(':visible') ?
    'Hide stale games' :
    'Show stale games'
  );
  Overview.updateSectionHeaderCounts('activeGames');
};

Overview.pageAddIntroText = function() {
  Overview.page.append($('<h1>', {'text': 'Welcome to Button Men!', }));

  var infopar;
  if (Config.siteType == 'development' || Config.siteType == 'staging') {
    infopar = $('<p>');
    infopar.append(
      'This is the <span style="color: red;">' +
      Config.siteType.toUpperCase() +
      '</span> version of ' +
      'the Buttonweavers implementation of ');
    infopar.append($('<a>', {
      'href': 'http://cheapass.com/free-games/button-men/',
      'text': 'Button Men',
    }));
    infopar.append('.');
    Overview.page.append(infopar);

    Overview.page.append($('<br />'));

    infopar = $('<p>');
    infopar.append(
      'If you\'re looking for the Button Men open alpha, please head over to ');
    infopar.append($('<a>', {
      'href': 'http://www.buttonweavers.com',
      'text': 'www.buttonweavers.com',
    }));
    infopar.append(' to start beating people up!');
    Overview.page.append(infopar);

    infopar = $('<p>');
    infopar.append(
      'If you\'re interested in joining the ' +
      Config.siteType +
      ' site as a tester, ' +
      'then check out our ');
    infopar.append($('<a>', {
      'href': 'https://github.com/buttonmen-dev/buttonmen/wiki/Tester-guide',
      'text': 'tester guide',
    }));
    infopar.append(', then Login using the menubar above, or ');
    infopar.append($('<a>', {
      'href': '/ui/create_user.html',
      'text': 'create an account',
    }));
    infopar.append('.');
    Overview.page.append(infopar);
  } else {
    infopar = $('<p>');
    infopar.append(
      'This is the alpha version of the Buttonweavers implementation of ');
    infopar.append($('<a>', {
      'href': 'http://www.cheapass.com/node/39',
      'text': 'Button Men',
    }));
    infopar.append('.');
    Overview.page.append(infopar);

    infopar = $('<p>');
    infopar.append(
      'Want to start beating people up?  Login using the menubar above, or ');
    infopar.append($('<a>', {
      'href': '/ui/create_user.html',
      'text': 'create an account',
    }));
    infopar.append('.');
    Overview.page.append(infopar);
  }
};

Overview.formDismissGame = function(e) {
  e.preventDefault();
  var args = { 'type': 'dismissGame', 'gameId': $(this).attr('data-gameId'), };
  var messages = {
    'ok': { 'type': 'fixed', 'text': 'Successfully dismissed game', },
    'notok': { 'type': 'server' },
  };
  Api.apiFormPost(args, messages, $(this), Overview.showLoggedInPage,
    Overview.showLoggedInPage);
};

// Redirect to the next new forum post if there is one
Overview.goToNextNewForumPost = function() {
  // If we're making this call automatically for the monitor, keep track of that
  var appendix = '';
  if (Api.automatedApiCall) {
    appendix = '?auto=true';
  }

  if (Api.forumNavigation.load_status == 'ok') {
    if (Api.forumNavigation.nextNewPostId !== null &&
        $.isNumeric(Api.forumNavigation.nextNewPostId)) {
      Env.window.location.href =
        'forum.html' + appendix +
          '#!threadId=' + Api.forumNavigation.nextNewPostThreadId +
          '&postId=' + Api.forumNavigation.nextNewPostId;
    }
  } else {
    // If there are no new posts (which presumably means the user read them but
    // left this page open while doing so), just show the forum overview
    Env.window.location.href = 'forum.html' + appendix;
  }
};


<|start_filename|>src/ui/js/Config.js<|end_filename|>
// Configuration settings that we don't expect to change often
var Config = { };

Config.siteType = 'production';


<|start_filename|>test/src/ui/js/test_Profile.js<|end_filename|>
module("Profile", {
  'setup': function() {
    BMTestUtils.ProfilePre = BMTestUtils.getAllElements();

    BMTestUtils.setupFakeLogin();

    // Create the profile_page div so functions have something to modify
    if (document.getElementById('profile_page') == null) {
      $('body').append($('<div>', {'id': 'env_message', }));
      $('body').append($('<div>', {'id': 'profile_page', }));
    }
  },
  'teardown': function(assert) {

    // Do not ignore intermittent failures in this test --- you
    // risk breaking the entire suite in hard-to-debug ways
    assert.equal(jQuery.active, 0,
      "All test functions MUST complete jQuery activity before exiting");

    // Delete all elements we expect this module to create

    // JavaScript variables
    delete Api.profile_info;
    delete Env.window.location.search;
    delete Profile.page;

    // Page elements
    $('#profile_page').remove();

    BMTestUtils.deleteEnvMessage();
    BMTestUtils.cleanupFakeLogin();

    // Fail if any other elements were added or removed
    BMTestUtils.ProfilePost = BMTestUtils.getAllElements();
    assert.deepEqual(
      BMTestUtils.ProfilePost, BMTestUtils.ProfilePre,
      "After testing, the page should have no unexpected element changes");
  }
});

// pre-flight test of whether the Profile module has been loaded
test("test_Profile_is_loaded", function(assert) {
  assert.ok(Profile, "The Profile namespace exists");
});

// The purpose of this test is to demonstrate that the flow of
// Profile.showLoggedInPage() is correct for a showXPage function, namely
// that it calls an API getter with a showStatePage function as a
// callback.
//
// Accomplish this by mocking the invoked functions
test("test_Profile.showLoggedInPage", function(assert) {
  expect(5);
  var cached_getProfile = Profile.getProfile;
  var cached_showStatePage = Profile.showPage;
  var getProfileCalled = false;
  Profile.showPage = function() {
    assert.ok(getProfileCalled, "Profile.getProfile is called before Profile.showPage");
  };
  Profile.getProfile = function(callback) {
    getProfileCalled = true;
    assert.equal(callback, Profile.showPage,
      "Profile.getProfile is called with Profile.showPage as an argument");
    callback();
  };

  Profile.showLoggedInPage();
  var item = document.getElementById('profile_page');
  assert.equal(item.nodeName, "DIV",
        "#profile_page is a div after showLoggedInPage() is called");

  Profile.getProfile = cached_getProfile;
  Profile.showPage = cached_showStatePage;
});

test("test_Profile.getProfile", function(assert) {
  stop();
  Env.window.location.search = '?player=tester';
  Profile.getProfile(function() {
    assert.ok(Api.profile_info, "Profile info parsed from server");
    if (Api.profile_info) {
      assert.equal(Api.profile_info.load_status, 'ok',
        "Profile info parsed successfully from server");
    }
    start();
  });
});

test("test_Profile.showPage", function(assert) {
  stop();
  Env.window.location.search = '?player=tester';
  Profile.getProfile(function() {
    Profile.showPage();
    var htmlout = Profile.page.html();
    assert.ok(htmlout.length > 0,
       "The created page should have nonzero contents");
    start();
  });
});

test("test_Profile.buildProfileTable", function(assert) {
  stop();
  Env.window.location.search = '?player=tester';
  Profile.getProfile(function() {
    var table = Profile.buildProfileTable();
    var htmlout = table.html();
    assert.ok(htmlout.match('February 29'), "Profile table content was generated");
    start();
  });
});

test("test_Profile.buildProfileTableRow", function(assert) {
  var tr = Profile.buildProfileTableRow('Things', 'something', 'nothing', true);
  var valueTd = tr.find('td.partialValue');
  assert.equal(valueTd.text(), 'something', 'Value should be in partialValue cell');

  var tr = Profile.buildProfileTableRow('Things', 'something', 'nothing', false);
  var valueTd = tr.find('td.value');
  assert.equal(valueTd.text(), 'something', 'Value should be in value cell');

  tr = Profile.buildProfileTableRow('Things', null, 'nothing', false);
  valueTd = tr.find('td.missingValue');
  assert.equal(valueTd.text(), 'nothing', 'Missing value should be in missingValue cell');

  tr = Profile.buildProfileTableRow('A', 'B', 'C', true, 'SomeClass');
  valueTd = tr.find('td.SomeClass');
  assert.equal(valueTd.text(), 'B', 'Row should include SomeClass as style');
});
