From d3d16823d454559b8f1b7a436efb297440b39e40 Mon Sep 17 00:00:00 2001 From: James Ong Date: Mon, 29 Sep 2025 22:58:39 +1000 Subject: [PATCH 01/11] - Added BBCode parsing to titles - Added logic to truncate titles with BBCode in both JS and PHP - Added basic infrastructure for testing the tournament UI - Added basic unit test of tournament description UI - Replaced Array.prototype.findIndex with more compatible Array.prototype.some --- src/api/DummyApiResponder.php | 7 +++ src/engine/BMInterfaceTournament.php | 36 +++++++++-- src/ui/js/Game.js | 2 +- src/ui/js/Overview.js | 9 ++- src/ui/js/Tournament.js | 16 ++++- src/ui/js/TournamentOverview.js | 7 ++- test/src/api/responderTournamentTest.php | 6 +- test/src/engine/BMInterfaceTournamentTest.php | 61 +++++++++++++++++++ test/src/ui/js/BMTestUtils.js | 15 +++++ test/src/ui/js/test_Newtournament.js | 2 +- test/src/ui/js/test_Tournament.js | 42 +++++++++---- 11 files changed, 174 insertions(+), 29 deletions(-) diff --git a/src/api/DummyApiResponder.php b/src/api/DummyApiResponder.php index 3d61069c3..dcd5a927e 100644 --- a/src/api/DummyApiResponder.php +++ b/src/api/DummyApiResponder.php @@ -224,6 +224,13 @@ protected function get_interface_response_loadGameData($args) { ); } + protected function get_interface_response_loadTournamentData($args) { + return $this->load_json_data_from_file( + 'loadTournamentData', + $args['tournament'] . '.json' + ); + } + protected function get_interface_response_countPendingGames() { return $this->load_json_data_from_file( 'countPendingGames', diff --git a/src/engine/BMInterfaceTournament.php b/src/engine/BMInterfaceTournament.php index 43e216d14..467376ba5 100644 --- a/src/engine/BMInterfaceTournament.php +++ b/src/engine/BMInterfaceTournament.php @@ -503,9 +503,10 @@ protected function generate_new_games(BMTournament $tournament) { array($gameData['buttonId1'], $gameData['buttonId2']) ); - $description = 'Round ' . $gameData['roundNumber']; - if ('' != $tournament->description) { - $description = $tournament->description . ' ' . $description; + $roundDescription = 'Tournament Round ' . $gameData['roundNumber']; + $tournDescription = $this->truncate_tournament_description($tournament->description); + if ('' != $tournDescription) { + $roundDescription = $tournDescription . ', ' . $roundDescription; } $interfaceResponse = $this->game()->create_game_from_button_ids( @@ -513,7 +514,7 @@ protected function generate_new_games(BMTournament $tournament) { array($gameData['buttonId1'], $gameData['buttonId2']), $buttonNames, $tournament->gameMaxWins, - $description, + $roundDescription, NULL, 0, // needs to be non-null, but also a non-player ID TRUE, @@ -531,6 +532,33 @@ protected function generate_new_games(BMTournament $tournament) { } } + /** + * Truncate a tournament description so that the tournament round number can be appended + * without exceeding the max length of the field in the database. + * + * This also aggressively removes BBCode markup if there is the possibility of + * breaking BBCode through truncation. + * + * @param string $description + */ + protected function truncate_tournament_description($description) { + if (strlen($description) > 230) { + $removedText = substr($description, 230); + if (strpos($removedText, ']') !== FALSE) { + // strip out all potential BBCode + $description = preg_replace( + '#\[([^\]]+?)(=[^\]]+?)?\](.+?)\[/\1\]#', + '$3', + $description + ); + } + + $description = substr($description, 0, 230) . '...'; + } + + return($description); + } + /** * Most of the tournament saving logic * diff --git a/src/ui/js/Game.js b/src/ui/js/Game.js index eccb1732c..4c237c5dd 100644 --- a/src/ui/js/Game.js +++ b/src/ui/js/Game.js @@ -1836,7 +1836,7 @@ Game.pageAddGameHeader = function(action_desc) { if (Api.game.description) { Game.page.append($('
', { - 'text': Api.game.description, + 'html': Env.applyBbCodeToHtml(Api.game.description), 'class': 'gameDescDisplay', })); } diff --git a/src/ui/js/Overview.js b/src/ui/js/Overview.js index bfb001737..d99d75d18 100644 --- a/src/ui/js/Overview.js +++ b/src/ui/js/Overview.js @@ -443,9 +443,12 @@ Overview.addScoreCol = function(gameRow, gameInfo) { Overview.addDescCol = function(gameRow, description) { var descText = ''; - if (typeof(description) == 'string') { - descText = description.substring(0, 30) + - ((description.length > 30) ? '...' : ''); + if (typeof(description) === 'string') { + var descriptionNoMarkup = + Env.applyBbCodeToHtml(description).replace(/<[^>]+>/g, ''); + + descText = descriptionNoMarkup.substring(0, 30) + + ((descriptionNoMarkup.length > 30) ? '...' : ''); } gameRow.append($('', { 'class': 'gameDescDisplay', diff --git a/src/ui/js/Tournament.js b/src/ui/js/Tournament.js index e1d0a7f82..6f16ec482 100644 --- a/src/ui/js/Tournament.js +++ b/src/ui/js/Tournament.js @@ -168,7 +168,8 @@ Tournament.pageAddTournamentHeader = function() { if (Api.tournament.description) { Tournament.page.append($('
', { - 'text': Api.tournament.description, + 'id': 'tournament_desc', + 'html': Env.applyBbCodeToHtml(Api.tournament.description), 'class': 'gameDescDisplay', })); } @@ -347,9 +348,18 @@ Tournament.pageAddWinnerInfo = function () { var winnerDiv = $('
'); Tournament.page.append(winnerDiv); - var winnerIdx = Api.tournament.remainCountArray.findIndex( - function(x) {return (x > 0);} + var winnerIdx; + var isWinnerFound = Api.tournament.remainCountArray.some( + function(item, idx) { + winnerIdx = idx; + return item > 0; + } ); + + if (!isWinnerFound) { + return; + } + var winnerPar = $('

', { 'class': 'winner_name', 'text': 'Winner: ' + Api.tournament.playerDataArray[winnerIdx].playerName diff --git a/src/ui/js/TournamentOverview.js b/src/ui/js/TournamentOverview.js index eab9a6a90..9caeba5dc 100644 --- a/src/ui/js/TournamentOverview.js +++ b/src/ui/js/TournamentOverview.js @@ -284,8 +284,11 @@ TournamentOverview.addTypeCol = function(tournamentRow, tournamentInfo) { TournamentOverview.addDescCol = function(tournamentRow, description) { var descText = ''; if (typeof(description) === 'string') { - descText = description.substring(0, 30) + - ((description.length > 30) ? '...' : ''); + var descriptionNoMarkup = + Env.applyBbCodeToHtml(description).replace(/<[^>]+>/g, ''); + + descText = descriptionNoMarkup.substring(0, 30) + + ((descriptionNoMarkup.length > 30) ? '...' : ''); } tournamentRow.append($('', { 'class': 'tournamentDescDisplay', diff --git a/test/src/api/responderTournamentTest.php b/test/src/api/responderTournamentTest.php index a36ca08a9..99ac8ab92 100644 --- a/test/src/api/responderTournamentTest.php +++ b/test/src/api/responderTournamentTest.php @@ -211,7 +211,7 @@ public function test_interface_tournament() { $gameOneExpData = $this->generate_init_expected_data_array($gameOneId, 'responder004', 'responder005', 1, 'SPECIFY_DICE'); $gameOneExpData['tournamentId'] = $tournamentId; $gameOneExpData['tournamentRoundNumber'] = 1; - $gameOneExpData['description'] = 'Round 1'; + $gameOneExpData['description'] = 'Tournament Round 1'; $gameOneExpData['currentPlayerIdx'] = FALSE; $gameOneExpData['creatorDataArray'] = array('creatorId' => 0, 'creatorName' => ''); $gameOneExpData['gameActionLog'][0]['message'] = 'Game created automatically'; @@ -243,7 +243,7 @@ public function test_interface_tournament() { $gameTwoExpData['gameSkillsInfo'] = $this->get_skill_info(array('Poison')); $gameTwoExpData['tournamentId'] = $tournamentId; $gameTwoExpData['tournamentRoundNumber'] = 1; - $gameTwoExpData['description'] = 'Round 1'; + $gameTwoExpData['description'] = 'Tournament Round 1'; $gameTwoExpData['activePlayerIdx'] = 0; $gameTwoExpData['playerWithInitiativeIdx'] = 0; $gameTwoExpData['creatorDataArray'] = array('creatorId' => 0, 'creatorName' => ''); @@ -363,7 +363,7 @@ public function test_interface_tournament() { $gameThreeExpData['gameSkillsInfo'] = $this->get_skill_info(array('Poison')); $gameThreeExpData['tournamentId'] = $tournamentId; $gameThreeExpData['tournamentRoundNumber'] = 2; - $gameThreeExpData['description'] = 'Round 2'; + $gameThreeExpData['description'] = 'Tournament Round 2'; $gameThreeExpData['activePlayerIdx'] = 1; $gameThreeExpData['playerWithInitiativeIdx'] = 1; $gameThreeExpData['currentPlayerIdx'] = 1; diff --git a/test/src/engine/BMInterfaceTournamentTest.php b/test/src/engine/BMInterfaceTournamentTest.php index 9f1ca4cff..f266ad192 100644 --- a/test/src/engine/BMInterfaceTournamentTest.php +++ b/test/src/engine/BMInterfaceTournamentTest.php @@ -18,4 +18,65 @@ public function test_create_tournament( ) { } + + public function test_truncate_tournament_description() { + $reflection = new ReflectionMethod($this->object, 'truncate_tournament_description'); + $reflection->setAccessible(true); + + $description = 'short'; + $shortDescription = $reflection->invoke($this->object, $description); + $this->assertEquals($description, $shortDescription, 'Short descriptions should not be truncated'); + + $description = '12345678901234567890123456789012345678901234567890' . + '12345678901234567890123456789012345678901234567890' . + '12345678901234567890123456789012345678901234567890' . + '12345678901234567890123456789012345678901234567890' . + '12345678901234567890123456789012345678901234567890' . + '12345678901234567890123456789012345678901234567890'; + $shortDescription = $reflection->invoke($this->object, $description); + $this->assertEquals( + '12345678901234567890123456789012345678901234567890' . + '12345678901234567890123456789012345678901234567890' . + '12345678901234567890123456789012345678901234567890' . + '12345678901234567890123456789012345678901234567890' . + '123456789012345678901234567890...', + $shortDescription, + 'Long text descriptions should be truncated appropriately' + ); + + $description = '[forum=1,6]text[/forum]456789012345678901234567890' . + '[forum=1,6]text[/forum]456789012345678901234567890' . + '[forum=1,6]text[/forum]456789012345678901234567890' . + '[forum=1,6]text[/forum]456789012345678901234567890' . + '[forum=1,6]text[/forum]456789012345678901234567890' . + '12345678901234567890123456789012345678901234567890'; + $shortDescription = $reflection->invoke($this->object, $description); + $this->assertEquals( + '[forum=1,6]text[/forum]456789012345678901234567890' . + '[forum=1,6]text[/forum]456789012345678901234567890' . + '[forum=1,6]text[/forum]456789012345678901234567890' . + '[forum=1,6]text[/forum]456789012345678901234567890' . + '[forum=1,6]text[/forum]4567890...', + $shortDescription, + 'Early markup should not be removed' + ); + + $description = '[forum=1,6]text[/forum]456789012345678901234567890' . + '[forum=1,6]text[/forum]456789012345678901234567890' . + '[forum=1,6]text[/forum]456789012345678901234567890' . + '[forum=1,6]text[/forum]456789012345678901234567890' . + '1234567890[forum=1,6]text[/forum]45678901234567890' . + '12345678901234567890123456789012345678901234567890'; + $shortDescription = $reflection->invoke($this->object, $description); + $this->assertEquals( + 'text456789012345678901234567890' . + 'text456789012345678901234567890' . + 'text456789012345678901234567890' . + 'text456789012345678901234567890' . + '1234567890text4567890123456789012345678901234567890' . + '123456789012345678901234567890...', + $shortDescription, + 'Late square close brackets should trigger BBCode removal' + ); + } } diff --git a/test/src/ui/js/BMTestUtils.js b/test/src/ui/js/BMTestUtils.js index 4420bff69..df109a693 100644 --- a/test/src/ui/js/BMTestUtils.js +++ b/test/src/ui/js/BMTestUtils.js @@ -46,10 +46,13 @@ BMTestUtils.getAllElements = function() { 'Loader': JSON.stringify(Loader, null, " "), 'Login': JSON.stringify(Login, null, " "), 'Newgame': JSON.stringify(Newgame, null, " "), + 'Newtournament': JSON.stringify(Newtournament, null, " "), 'Newuser': JSON.stringify(Newuser, null, " "), 'OpenGames': JSON.stringify(OpenGames, null, " "), 'Overview': JSON.stringify(Overview, null, " "), 'Profile': JSON.stringify(Profile, null, " "), + 'Tournament': JSON.stringify(Tournament, null, " "), + 'TournamentOverview': JSON.stringify(TournamentOverview, null, " "), 'UserPrefs': JSON.stringify(UserPrefs, null, " "), 'Verify': JSON.stringify(Verify, null, " "), }; @@ -132,6 +135,14 @@ BMTestUtils.testGameId = function(gameDesc) { if (gameDesc == 'NOGAME') { return '10000000'; } }; +// For each tournament reported by responderTest which we use in UI +// tests, set a friendly name for tracking purposes. These values +// need to be kept in sync with responderTest in order for anything +// good to happen. +BMTestUtils.testTournamentId = function(tournamentDesc) { + if (tournamentDesc == 'default') { return '1'; } +}; + // We don't currently usually test reading the URL bar contents, because // that's hard to do within QUnit, but rather override those contents // with hardcoded values that we want to test. @@ -143,6 +154,10 @@ BMTestUtils.overrideGetParameterByName = function() { return BMTestUtils.testGameId(BMTestUtils.GameType); } + if (name == 'tournament') { + return BMTestUtils.testTournamentId(BMTestUtils.TournamentType); + } + // always return the userid associated with tester1 in the fake data if (name == 'id') { return '1'; diff --git a/test/src/ui/js/test_Newtournament.js b/test/src/ui/js/test_Newtournament.js index f7a57815a..b9abe90e1 100644 --- a/test/src/ui/js/test_Newtournament.js +++ b/test/src/ui/js/test_Newtournament.js @@ -26,7 +26,7 @@ module("Newtournament", { delete Api.player; delete Newtournament.page; delete Newtournament.form; -// delete Newtournament.justCreatedGame; + delete Newtournament.justCreatedTournament; Login.pageModule = null; Newtournament.activity = {}; diff --git a/test/src/ui/js/test_Tournament.js b/test/src/ui/js/test_Tournament.js index 76d99f409..6ca6e046e 100644 --- a/test/src/ui/js/test_Tournament.js +++ b/test/src/ui/js/test_Tournament.js @@ -153,17 +153,15 @@ test("test_Tournament.getCurrentTournament", function(assert) { }); test("test_Tournament.showStatePage", function(assert) { -// stop(); -// BMTestUtils.GameType = 'frasquito_wiseman_specifydice'; -// Tournament.getCurrentTournament(function() { -// Tournament.showStatePage(); -// var htmlout = Tournament.page.html(); -// assert.ok(htmlout.length > 0, -// "The created page should have nonzero contents"); -// assert.ok(htmlout.match('vacation16.png'), -// "The game UI contains a vacation icon when the API data reports that one player is on vacation"); -// start(); -// }); + stop(); + BMTestUtils.TournamentType = 'default'; + Tournament.getCurrentTournament(function() { + Tournament.showStatePage(); + var htmlout = Tournament.page.html(); + assert.ok(htmlout.length > 0, + "The created page should have nonzero contents"); + start(); + }); }); test("test_Tournament.showTournamentContents", function(assert) { @@ -171,7 +169,27 @@ test("test_Tournament.showTournamentContents", function(assert) { }); test("test_Tournament.pageAddTournamentHeader", function(assert) { - + stop(); + BMTestUtils.TournamentType = 'default'; + Tournament.getCurrentTournament(function() { + Api.tournament.description = 'header'; + Tournament.showStatePage(); + Tournament.pageAddTournamentHeader(); + var htmlout = Tournament.page.html(); + assert.ok(htmlout.length > 0, + "The created page should have nonzero contents"); + // now test the header contents + //console.log(Api.tournament); + //console.log(htmlout); + + + var item = document.getElementById('tournament_desc'); + assert.equal(item.nodeName, "DIV", + "#tournament_desc is a div after redrawTournamentPageSuccess() is called"); + assert.equal($(item).html(), 'header', 'Header text should be correct'); + + start(); + }); }); test("test_Tournament.pageAddDismissTournamentLink", function(assert) { From 01432cd1c974d675d571bde233f9c185798b8dfa Mon Sep 17 00:00:00 2001 From: James Ong Date: Sat, 3 Jan 2026 16:43:08 +1100 Subject: [PATCH 02/11] Added unit test for display of tournament description including BBCode --- src/ui/js/Tournament.js | 30 +++++++++++------- test/src/ui/js/test_Tournament.js | 52 ++++++++++++++++++++++++++----- 2 files changed, 63 insertions(+), 19 deletions(-) diff --git a/src/ui/js/Tournament.js b/src/ui/js/Tournament.js index 6f16ec482..6583062cc 100644 --- a/src/ui/js/Tournament.js +++ b/src/ui/js/Tournament.js @@ -166,14 +166,7 @@ Tournament.pageAddTournamentHeader = function() { // bgcolor = Tournament.color.opponent; // } - if (Api.tournament.description) { - Tournament.page.append($('

', { - 'id': 'tournament_desc', - 'html': Env.applyBbCodeToHtml(Api.tournament.description), - 'class': 'gameDescDisplay', - })); - } - + Tournament.pageAddTournamentDescription(); Tournament.page.append($('
')); Tournament.pageAddTournamentInfo(); @@ -307,8 +300,23 @@ Tournament.formFollowTournament = function(e) { Tournament.showLoggedInPage); }; +Tournament.pageAddTournamentDescription = function () { + if (Api.tournament.description) { + Tournament.page.append($('
', { + 'id': 'tournament_desc', + 'html': Env.applyBbCodeToHtml(Api.tournament.description), + 'class': 'gameDescDisplay', + })); + } +}; + Tournament.pageAddTournamentInfo = function () { - var infoDiv = $('
'); + var infoDiv = $( + '
', + { + 'id': 'tournament_info', + } + ); Tournament.page.append(infoDiv); var tournamentTypePar = $('

', { @@ -350,9 +358,9 @@ Tournament.pageAddWinnerInfo = function () { var winnerIdx; var isWinnerFound = Api.tournament.remainCountArray.some( - function(item, idx) { + function(remainCount, idx) { winnerIdx = idx; - return item > 0; + return remainCount > 0; } ); diff --git a/test/src/ui/js/test_Tournament.js b/test/src/ui/js/test_Tournament.js index 6ca6e046e..fdb4f2c2c 100644 --- a/test/src/ui/js/test_Tournament.js +++ b/test/src/ui/js/test_Tournament.js @@ -172,21 +172,20 @@ test("test_Tournament.pageAddTournamentHeader", function(assert) { stop(); BMTestUtils.TournamentType = 'default'; Tournament.getCurrentTournament(function() { - Api.tournament.description = 'header'; + Api.tournament.description = 'description'; Tournament.showStatePage(); Tournament.pageAddTournamentHeader(); var htmlout = Tournament.page.html(); assert.ok(htmlout.length > 0, "The created page should have nonzero contents"); - // now test the header contents - //console.log(Api.tournament); - //console.log(htmlout); + var tournHeader = $('#tournament_id'); + var tournDesc = $('#tournament_desc'); + var tournInfo = $('#tournament_info'); - var item = document.getElementById('tournament_desc'); - assert.equal(item.nodeName, "DIV", - "#tournament_desc is a div after redrawTournamentPageSuccess() is called"); - assert.equal($(item).html(), 'header', 'Header text should be correct'); + assert.ok(tournHeader.is('div'), 'Tournament header should be a div'); + assert.ok(tournDesc.is('div'), 'Tournament description should be a div'); + assert.ok(tournInfo.is('div'), 'Tournament info should be a div'); start(); }); @@ -216,6 +215,43 @@ test("test_Tournament.formFollowTournament", function(assert) { }); +test("test_Tournament.pageAddTournamentDescription", function(assert) { + stop(); + BMTestUtils.TournamentType = 'default'; + Tournament.getCurrentTournament(function() { + Api.tournament.description = + '[forum=1,6]text[/forum]456789012345678901234567890' + + '[forum=1,6]text[/forum]456789012345678901234567890' + + '[forum=1,6]text[/forum]456789012345678901234567890' + + '[forum=1,6]text[/forum]456789012345678901234567890' + + '[forum=1,6]text[/forum]4567890...'; + Tournament.showStatePage(); + Tournament.pageAddTournamentDescription(); + + var tournDesc = $('#tournament_desc'); + + var convertedDescription = + 'text' + + '456789012345678901234567890' + + 'text' + + '456789012345678901234567890' + + 'text' + + '456789012345678901234567890' + + 'text' + + '456789012345678901234567890' + + 'text' + + '4567890...'; + + assert.equal( + tournDesc.html(), + convertedDescription, + 'Description text should be correct' + ); + + start(); + }); +}); + test("test_Tournament.pageAddTournamentInfo", function(assert) { }); From b7bb41718d5521dbd32e2849de7e4bde443e7523 Mon Sep 17 00:00:00 2001 From: James Ong Date: Sun, 4 Jan 2026 16:45:26 +1100 Subject: [PATCH 03/11] Added new function Env.removeBbCodeFromHtml to strip BBCode from descriptions on the Overview page --- src/ui/js/Env.js | 124 ++++++++++++++++++++++++++++++++++++++++++ src/ui/js/Overview.js | 3 +- 2 files changed, 125 insertions(+), 2 deletions(-) diff --git a/src/ui/js/Env.js b/src/ui/js/Env.js index 3e39cab94..795bc4ff5 100644 --- a/src/ui/js/Env.js +++ b/src/ui/js/Env.js @@ -524,6 +524,130 @@ Env.applyBbCodeToHtml = function(htmlToParse) { return outputHtml; }; +Env.removeBbCodeFromHtml = function(htmlToParse) { + // This is all rather more complicated than one might expect, but any attempt + // to parse BB code using simple regular expressions rather than tokenization + // is in the same family as parsing HTML with regular expressions, which + // summons Zalgo. + // (See: http://stackoverflow.com/ + // questions/1732348/regex-match-open-tags-except-xhtml-self-contained-tags) + + var replacements = { + 'b': {}, + 'i': {}, + 'u': {}, + 's': {}, + 'code': {}, + 'spoiler': {}, + 'quote': {}, + 'game': { + 'isAtomic': true, + }, + 'player': { + 'isAtomic': true, + }, + 'button': { + 'isAtomic': true, + }, + 'set': { + 'isAtomic': true, + }, + 'tourn': { + 'isAtomic': true, + }, + 'wiki': { + 'isAtomic': true, + }, + 'issue': { + 'isAtomic': true, + }, + 'forum': {}, + '[': { + 'isAtomic': true, + }, + }; + + var outputHtml = ''; + var tagStack = []; + + // We want to build a pattern that we can use to identify any single + // BB code start tag + var allStartTagsPattern = ''; + $.each(replacements, function(tagName) { + if (allStartTagsPattern !== '') { + allStartTagsPattern += '|'; + } + // Matches, e.g., '[ b ]' or '[game = "123"]' + // The (?:... part means that we want parentheses around the whole + // thing (so we we can OR it together with other ones), but we don't + // want to capture the value of the whole thing as a group + allStartTagsPattern += + '(?:\\[(' + Env.escapeRegexp(tagName) + ')(?:=([^\\]]*?))?])'; + }); + + var tagName; + + while (htmlToParse) { + var currentPattern = allStartTagsPattern; + if (tagStack.length !== 0) { + // The tag that was most recently opened + tagName = tagStack[tagStack.length - 1]; + // Matches '[/i]' et al. + // (so that we can spot the end of the current tag as well) + currentPattern += + '|(?:\\[(/' + Env.escapeRegexp(tagName) + ')])'; + } + // The first group should be non-greedy (hence the ?), and the last one + // should be greedy, so that nested tags work right + // (E.g., in '...blah[/quote] blah [/quote] blah', we want the first .* + // to end at the first [/quote], not the second) + currentPattern = '^(.*?)(?:' + currentPattern + ')(.*)$'; + // case-insensitive, multi-line + var regExp = new RegExp(currentPattern, 'im'); + + var match = htmlToParse.match(regExp); + if (match) { + var stuffBeforeTag = match[1]; + // javascript apparently believes that capture groups that don't + // match anything are just important as those that do. So we need + // to do some acrobatics to find the ones we actually care about. + // (match[0] is the whole matched string; match[1] is the stuff before + // the tag. So we start with match[2].) + tagName = ''; + for (var i = 2; i < match.length; i++) { + tagName = match[i]; + if (tagName) { + break; + } + } + tagName = tagName.toLowerCase(); + var stuffAfterTag = match[match.length - 1]; + + outputHtml += stuffBeforeTag; + if (tagName.substring(0, 1) === '/') { + // If we've found our closing tag, we can finish the current tag and + // pop it off the stack + tagName = tagStack.pop(); + } else { + if (!replacements[tagName].isAtomic) { + // If there's a closing tag coming along later, push this tag + // on the stack so we'll know we're waiting on it + tagStack.push(tagName); + } + } + + htmlToParse = stuffAfterTag; + } else { + // If we don't find any more BB code tags that we're interested in, + // then we must have reached the end + outputHtml += htmlToParse; + htmlToParse = ''; + } + } + + return outputHtml; +}; + Env.escapeRegexp = function(str) { return str.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); }; diff --git a/src/ui/js/Overview.js b/src/ui/js/Overview.js index d99d75d18..770ed47cf 100644 --- a/src/ui/js/Overview.js +++ b/src/ui/js/Overview.js @@ -444,8 +444,7 @@ Overview.addScoreCol = function(gameRow, gameInfo) { Overview.addDescCol = function(gameRow, description) { var descText = ''; if (typeof(description) === 'string') { - var descriptionNoMarkup = - Env.applyBbCodeToHtml(description).replace(/<[^>]+>/g, ''); + var descriptionNoMarkup = Env.removeBbCodeFromHtml(description); descText = descriptionNoMarkup.substring(0, 30) + ((descriptionNoMarkup.length > 30) ? '...' : ''); From 8f62080925ec02ba9214fa1e2b3713da8c00f60b Mon Sep 17 00:00:00 2001 From: James Ong Date: Sun, 4 Jan 2026 16:59:08 +1100 Subject: [PATCH 04/11] Added QUnit test for new function --- test/src/ui/js/test_Env.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/src/ui/js/test_Env.js b/test/src/ui/js/test_Env.js index a81d66ed0..2b10fce8d 100644 --- a/test/src/ui/js/test_Env.js +++ b/test/src/ui/js/test_Env.js @@ -272,6 +272,12 @@ test("test_Env.applyBbCodeToHtml", function(assert) { assert.ok(holder.find('.chatItalic').length == 1, '[i] tag should be converted to HTML'); }); +test("test_Env.removeBbCodeFromHtml", function(assert) { + var rawHtml = 'HTML
[i]BB Code[/i]'; + var newHtml = Env.removeBbCodeFromHtml(rawHtml); + assert.equal(newHtml, 'HTML
BB Code', 'Stripped-down HTML should be correct'); +}); + test("test_Env.escapeRegexp", function(assert) { var rawText = 'example.com'; var escapedPattern = Env.escapeRegexp(rawText); @@ -339,7 +345,7 @@ test("test_Env.toggleSpoiler", function(assert) { var spoiler = $('', { 'class': 'chatSpoiler' }); var eventTriggerSpan = {'target': {'tagName': 'span'}}; var eventTriggerAnchor = {'target': {'tagName': 'a'}}; - + Env.toggleSpoiler.call(spoiler, eventTriggerSpan); assert.ok(spoiler.hasClass('chatExposedSpoiler'), 'Spoiler should be styled as revealed'); From 9978a374905100e43784bb2fe1fc80ced505cf01 Mon Sep 17 00:00:00 2001 From: James Ong Date: Thu, 8 Jan 2026 18:24:01 +1100 Subject: [PATCH 05/11] Used new function to strip BBCode on the tournament overview page --- src/ui/js/TournamentOverview.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ui/js/TournamentOverview.js b/src/ui/js/TournamentOverview.js index 9caeba5dc..97902a75e 100644 --- a/src/ui/js/TournamentOverview.js +++ b/src/ui/js/TournamentOverview.js @@ -284,8 +284,7 @@ TournamentOverview.addTypeCol = function(tournamentRow, tournamentInfo) { TournamentOverview.addDescCol = function(tournamentRow, description) { var descText = ''; if (typeof(description) === 'string') { - var descriptionNoMarkup = - Env.applyBbCodeToHtml(description).replace(/<[^>]+>/g, ''); + var descriptionNoMarkup = Env.removeBbCodeFromHtml(description); descText = descriptionNoMarkup.substring(0, 30) + ((descriptionNoMarkup.length > 30) ? '...' : ''); From 60d3f54b8033e7ba02726d5a378a76b880f74017 Mon Sep 17 00:00:00 2001 From: James Ong Date: Sun, 11 Jan 2026 19:22:48 +1100 Subject: [PATCH 06/11] - Simplified and refactored logic to strip BBCode from overly long tournament descriptions - Replaced magic numbers with an actual calculation of the maximum allowable length --- src/engine/BMInterfaceTournament.php | 83 +++++++++++++++---- test/src/engine/BMInterfaceTournamentTest.php | 80 ++++++++++++------ 2 files changed, 122 insertions(+), 41 deletions(-) diff --git a/src/engine/BMInterfaceTournament.php b/src/engine/BMInterfaceTournament.php index 467376ba5..c163a1533 100644 --- a/src/engine/BMInterfaceTournament.php +++ b/src/engine/BMInterfaceTournament.php @@ -504,7 +504,10 @@ protected function generate_new_games(BMTournament $tournament) { ); $roundDescription = 'Tournament Round ' . $gameData['roundNumber']; - $tournDescription = $this->truncate_tournament_description($tournament->description); + $tournDescription = $this->truncate_tournament_description( + $tournament->description, + $roundDescription + ); if ('' != $tournDescription) { $roundDescription = $tournDescription . ', ' . $roundDescription; } @@ -541,22 +544,74 @@ protected function generate_new_games(BMTournament $tournament) { * * @param string $description */ - protected function truncate_tournament_description($description) { - if (strlen($description) > 230) { - $removedText = substr($description, 230); - if (strpos($removedText, ']') !== FALSE) { - // strip out all potential BBCode - $description = preg_replace( - '#\[([^\]]+?)(=[^\]]+?)?\](.+?)\[/\1\]#', - '$3', - $description - ); - } + protected function truncate_tournament_description( + $tournDescription, + $roundDescription + ) { + // check if appending ", " followed by the tournament round description + // would cause the auto-generated game description to exceed the maximum + // length allowed for the tournament description + if (strlen($tournDescription) + 2 + strlen($roundDescription) > + ApiSpec::TOURNAMENT_DESCRIPTION_MAX_LENGTH) { + + // try to strip out non-essential BBCode first + $strippedDescription = $this->strip_nonessential_bbcode($tournDescription); + } else { + $strippedDescription = $tournDescription; + } - $description = substr($description, 0, 230) . '...'; + if (strlen($strippedDescription) + 2 + strlen($roundDescription) > + ApiSpec::TOURNAMENT_DESCRIPTION_MAX_LENGTH) { + // truncate the tournament description so that there is space for + // "..., " followed by the tournament round description + $truncDescription = substr( + $strippedDescription, + 0, + ApiSpec::TOURNAMENT_DESCRIPTION_MAX_LENGTH - 5 - strlen($roundDescription) + ) . '...'; + } else { + $truncDescription = $strippedDescription; } - return($description); + return($truncDescription); + } + + /** + * Strip non-essential BBCode from a string + * + * @param string $text + * @return string + */ + protected function strip_nonessential_bbcode($text) { + $tags = array( + 'b', + 'i', + 'u', + 's', + 'code', + 'quote', + 'forum' + ); + + // this regular expression is + // \[ match opening square bracket + // \/? match optional forward slash + // (?: non-capturing group 1 start + // tag1|tag2|... match one of the tags + // ) non-capturing group 1 end + // (?: non-capturing group 2 start + // = match a literal equal character + // [^=\]]+? match at least one character that is not equals or + // close square bracket, as few times as possible + // )? non-capturing group 2 end, this group is optional + // \] match closing square bracket + $strippedText = preg_replace( + '#\[\/?(?:' . implode('|', $tags) . ')(?:=[^=\]]+?)?\]#', + '', + $text + ); + + return $strippedText; } /** diff --git a/test/src/engine/BMInterfaceTournamentTest.php b/test/src/engine/BMInterfaceTournamentTest.php index f266ad192..dcb74ae07 100644 --- a/test/src/engine/BMInterfaceTournamentTest.php +++ b/test/src/engine/BMInterfaceTournamentTest.php @@ -19,12 +19,17 @@ public function test_create_tournament( } + /** + * @covers BMInterfaceTournament::truncate_tournament_description + */ public function test_truncate_tournament_description() { $reflection = new ReflectionMethod($this->object, 'truncate_tournament_description'); $reflection->setAccessible(true); + $roundDescription = 'Tournament Round 1'; + $description = 'short'; - $shortDescription = $reflection->invoke($this->object, $description); + $shortDescription = $reflection->invoke($this->object, $description, $roundDescription); $this->assertEquals($description, $shortDescription, 'Short descriptions should not be truncated'); $description = '12345678901234567890123456789012345678901234567890' . @@ -33,50 +38,71 @@ public function test_truncate_tournament_description() { '12345678901234567890123456789012345678901234567890' . '12345678901234567890123456789012345678901234567890' . '12345678901234567890123456789012345678901234567890'; - $shortDescription = $reflection->invoke($this->object, $description); + $shortDescription = $reflection->invoke($this->object, $description, $roundDescription); $this->assertEquals( '12345678901234567890123456789012345678901234567890' . '12345678901234567890123456789012345678901234567890' . '12345678901234567890123456789012345678901234567890' . '12345678901234567890123456789012345678901234567890' . - '123456789012345678901234567890...', + '12345678901234567890123456789012...', $shortDescription, 'Long text descriptions should be truncated appropriately' ); - $description = '[forum=1,6]text[/forum]456789012345678901234567890' . - '[forum=1,6]text[/forum]456789012345678901234567890' . - '[forum=1,6]text[/forum]456789012345678901234567890' . - '[forum=1,6]text[/forum]456789012345678901234567890' . - '[forum=1,6]text[/forum]456789012345678901234567890' . + $description = '[forum=1,6]text[/forum]56789012345678901234567890' . + '[forum=1,6]text[/forum]56789012345678901234567890' . + '[forum=1,6]text[/forum]56789012345678901234567890' . + '[forum=1,6]text[/forum]56789012345678901234567890' . + '[forum=1,6]text[/forum]56789012345678901234567890' . '12345678901234567890123456789012345678901234567890'; - $shortDescription = $reflection->invoke($this->object, $description); + $shortDescription = $reflection->invoke($this->object, $description, $roundDescription); $this->assertEquals( - '[forum=1,6]text[/forum]456789012345678901234567890' . - '[forum=1,6]text[/forum]456789012345678901234567890' . - '[forum=1,6]text[/forum]456789012345678901234567890' . - '[forum=1,6]text[/forum]456789012345678901234567890' . - '[forum=1,6]text[/forum]4567890...', + 'text56789012345678901234567890' . + 'text56789012345678901234567890' . + 'text56789012345678901234567890' . + 'text56789012345678901234567890' . + 'text56789012345678901234567890' . + '12345678901234567890123456789012345678901234567890', $shortDescription, - 'Early markup should not be removed' + 'Markup should be removed even with no late BBCode' ); - $description = '[forum=1,6]text[/forum]456789012345678901234567890' . - '[forum=1,6]text[/forum]456789012345678901234567890' . - '[forum=1,6]text[/forum]456789012345678901234567890' . - '[forum=1,6]text[/forum]456789012345678901234567890' . + $description = '[forum=1,6]text[/forum]56789012345678901234567890' . + '[forum=1,6]text[/forum]56789012345678901234567890' . + '[forum=1,6]text[/forum]56789012345678901234567890' . + '[forum=1,6]text[/forum]56789012345678901234567890' . '1234567890[forum=1,6]text[/forum]45678901234567890' . '12345678901234567890123456789012345678901234567890'; - $shortDescription = $reflection->invoke($this->object, $description); + $shortDescription = $reflection->invoke($this->object, $description, $roundDescription); $this->assertEquals( - 'text456789012345678901234567890' . - 'text456789012345678901234567890' . - 'text456789012345678901234567890' . - 'text456789012345678901234567890' . - '1234567890text4567890123456789012345678901234567890' . - '123456789012345678901234567890...', + 'text56789012345678901234567890' . + 'text56789012345678901234567890' . + 'text56789012345678901234567890' . + 'text56789012345678901234567890' . + '1234567890text45678901234567890' . + '12345678901234567890123456789012345678901234567890', $shortDescription, - 'Late square close brackets should trigger BBCode removal' + 'Markup should be removed with late BBCode' + ); + } + + /** + * @covers BMInterfaceTournament::strip_nonessential_bbcode + */ + public function test_strip_nonessential_bbcode() { + $reflection = new ReflectionMethod($this->object, 'strip_nonessential_bbcode'); + $reflection->setAccessible(true); + + $text = '[button=Abe Caine] is [b][i]very[/i] annoying[/b] ' . + '[spoiler]according to [player=tasha][/spoiler], ' . + 'see [forum=1335,32790]this forum thread[/forum]'; + $strippedText = $reflection->invoke($this->object, $text); + $this->assertEquals( + '[button=Abe Caine] is very annoying ' . + '[spoiler]according to [player=tasha][/spoiler], ' . + 'see this forum thread', + $strippedText, + 'BBCode stripping should be correct' ); } } From 3439818053a7b96a436b2eddef457952691067e8 Mon Sep 17 00:00:00 2001 From: James Ong Date: Sun, 11 Jan 2026 20:10:41 +1100 Subject: [PATCH 07/11] Simplified regex further, similar to Env.removeBbCodeFromHtml --- src/engine/BMInterfaceTournament.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/engine/BMInterfaceTournament.php b/src/engine/BMInterfaceTournament.php index c163a1533..bf276cc76 100644 --- a/src/engine/BMInterfaceTournament.php +++ b/src/engine/BMInterfaceTournament.php @@ -601,12 +601,14 @@ protected function strip_nonessential_bbcode($text) { // ) non-capturing group 1 end // (?: non-capturing group 2 start // = match a literal equal character - // [^=\]]+? match at least one character that is not equals or + // [^]]+? match at least one character that is not // close square bracket, as few times as possible + // (note that the closing bracket doesn't need to + // be escaped directly after the ^ in the character group) // )? non-capturing group 2 end, this group is optional // \] match closing square bracket $strippedText = preg_replace( - '#\[\/?(?:' . implode('|', $tags) . ')(?:=[^=\]]+?)?\]#', + '#\[\/?(?:' . implode('|', $tags) . ')(?:=[^]]+?)?\]#', '', $text ); From cd5400bc1712dcf99e10336ab3301231118e697d Mon Sep 17 00:00:00 2001 From: James Ong Date: Sun, 11 Jan 2026 22:01:03 +1100 Subject: [PATCH 08/11] Fixed PHPMD error --- src/engine/BMInterfaceTournament.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/engine/BMInterfaceTournament.php b/src/engine/BMInterfaceTournament.php index bf276cc76..62755fd08 100644 --- a/src/engine/BMInterfaceTournament.php +++ b/src/engine/BMInterfaceTournament.php @@ -553,7 +553,6 @@ protected function truncate_tournament_description( // length allowed for the tournament description if (strlen($tournDescription) + 2 + strlen($roundDescription) > ApiSpec::TOURNAMENT_DESCRIPTION_MAX_LENGTH) { - // try to strip out non-essential BBCode first $strippedDescription = $this->strip_nonessential_bbcode($tournDescription); } else { @@ -601,7 +600,7 @@ protected function strip_nonessential_bbcode($text) { // ) non-capturing group 1 end // (?: non-capturing group 2 start // = match a literal equal character - // [^]]+? match at least one character that is not + // [^]]+? match at least one character that is not a // close square bracket, as few times as possible // (note that the closing bracket doesn't need to // be escaped directly after the ^ in the character group) From b2961aff1f951b496f3f26dc202228369e2c9cfd Mon Sep 17 00:00:00 2001 From: James Ong Date: Thu, 15 Jan 2026 23:03:54 +1100 Subject: [PATCH 09/11] - Removed truncation and BBCode removal code from backend - Made game description field 50 characters longer in database to account for autogenerated tournament suffix --- deploy/database/schema.game.sql | 2 +- .../03071_long_tournament_description.sql | 1 + src/engine/BMInterfaceTournament.php | 94 ++----------------- test/src/engine/BMInterfaceTournamentTest.php | 86 ----------------- 4 files changed, 9 insertions(+), 174 deletions(-) create mode 100644 deploy/database/updates/03071_long_tournament_description.sql diff --git a/deploy/database/schema.game.sql b/deploy/database/schema.game.sql index 13366dc2d..c7a091833 100644 --- a/deploy/database/schema.game.sql +++ b/deploy/database/schema.game.sql @@ -17,7 +17,7 @@ CREATE TABLE game ( last_winner_id SMALLINT UNSIGNED, tournament_id SMALLINT UNSIGNED, tournament_round_number SMALLINT UNSIGNED, - description VARCHAR(255) NOT NULL, + description VARCHAR(305) NOT NULL, chat TEXT, previous_game_id MEDIUMINT UNSIGNED, FOREIGN KEY (previous_game_id) REFERENCES game(id) diff --git a/deploy/database/updates/03071_long_tournament_description.sql b/deploy/database/updates/03071_long_tournament_description.sql new file mode 100644 index 000000000..801a4547e --- /dev/null +++ b/deploy/database/updates/03071_long_tournament_description.sql @@ -0,0 +1 @@ +ALTER TABLE game MODIFY description VARCHAR(305) NOT NULL; diff --git a/src/engine/BMInterfaceTournament.php b/src/engine/BMInterfaceTournament.php index 62755fd08..42e1c06cd 100644 --- a/src/engine/BMInterfaceTournament.php +++ b/src/engine/BMInterfaceTournament.php @@ -504,12 +504,12 @@ protected function generate_new_games(BMTournament $tournament) { ); $roundDescription = 'Tournament Round ' . $gameData['roundNumber']; - $tournDescription = $this->truncate_tournament_description( - $tournament->description, - $roundDescription - ); - if ('' != $tournDescription) { - $roundDescription = $tournDescription . ', ' . $roundDescription; + $tournDescription = $tournament->description; + + if ('' == trim($tournDescription)) { + $tournDescription = $roundDescription; + } else { + $tournDescription = $tournDescription . ' • ' . $roundDescription; } $interfaceResponse = $this->game()->create_game_from_button_ids( @@ -517,7 +517,7 @@ protected function generate_new_games(BMTournament $tournament) { array($gameData['buttonId1'], $gameData['buttonId2']), $buttonNames, $tournament->gameMaxWins, - $roundDescription, + $tournDescription, NULL, 0, // needs to be non-null, but also a non-player ID TRUE, @@ -535,86 +535,6 @@ protected function generate_new_games(BMTournament $tournament) { } } - /** - * Truncate a tournament description so that the tournament round number can be appended - * without exceeding the max length of the field in the database. - * - * This also aggressively removes BBCode markup if there is the possibility of - * breaking BBCode through truncation. - * - * @param string $description - */ - protected function truncate_tournament_description( - $tournDescription, - $roundDescription - ) { - // check if appending ", " followed by the tournament round description - // would cause the auto-generated game description to exceed the maximum - // length allowed for the tournament description - if (strlen($tournDescription) + 2 + strlen($roundDescription) > - ApiSpec::TOURNAMENT_DESCRIPTION_MAX_LENGTH) { - // try to strip out non-essential BBCode first - $strippedDescription = $this->strip_nonessential_bbcode($tournDescription); - } else { - $strippedDescription = $tournDescription; - } - - if (strlen($strippedDescription) + 2 + strlen($roundDescription) > - ApiSpec::TOURNAMENT_DESCRIPTION_MAX_LENGTH) { - // truncate the tournament description so that there is space for - // "..., " followed by the tournament round description - $truncDescription = substr( - $strippedDescription, - 0, - ApiSpec::TOURNAMENT_DESCRIPTION_MAX_LENGTH - 5 - strlen($roundDescription) - ) . '...'; - } else { - $truncDescription = $strippedDescription; - } - - return($truncDescription); - } - - /** - * Strip non-essential BBCode from a string - * - * @param string $text - * @return string - */ - protected function strip_nonessential_bbcode($text) { - $tags = array( - 'b', - 'i', - 'u', - 's', - 'code', - 'quote', - 'forum' - ); - - // this regular expression is - // \[ match opening square bracket - // \/? match optional forward slash - // (?: non-capturing group 1 start - // tag1|tag2|... match one of the tags - // ) non-capturing group 1 end - // (?: non-capturing group 2 start - // = match a literal equal character - // [^]]+? match at least one character that is not a - // close square bracket, as few times as possible - // (note that the closing bracket doesn't need to - // be escaped directly after the ^ in the character group) - // )? non-capturing group 2 end, this group is optional - // \] match closing square bracket - $strippedText = preg_replace( - '#\[\/?(?:' . implode('|', $tags) . ')(?:=[^]]+?)?\]#', - '', - $text - ); - - return $strippedText; - } - /** * Most of the tournament saving logic * diff --git a/test/src/engine/BMInterfaceTournamentTest.php b/test/src/engine/BMInterfaceTournamentTest.php index dcb74ae07..2ebe8754f 100644 --- a/test/src/engine/BMInterfaceTournamentTest.php +++ b/test/src/engine/BMInterfaceTournamentTest.php @@ -19,90 +19,4 @@ public function test_create_tournament( } - /** - * @covers BMInterfaceTournament::truncate_tournament_description - */ - public function test_truncate_tournament_description() { - $reflection = new ReflectionMethod($this->object, 'truncate_tournament_description'); - $reflection->setAccessible(true); - - $roundDescription = 'Tournament Round 1'; - - $description = 'short'; - $shortDescription = $reflection->invoke($this->object, $description, $roundDescription); - $this->assertEquals($description, $shortDescription, 'Short descriptions should not be truncated'); - - $description = '12345678901234567890123456789012345678901234567890' . - '12345678901234567890123456789012345678901234567890' . - '12345678901234567890123456789012345678901234567890' . - '12345678901234567890123456789012345678901234567890' . - '12345678901234567890123456789012345678901234567890' . - '12345678901234567890123456789012345678901234567890'; - $shortDescription = $reflection->invoke($this->object, $description, $roundDescription); - $this->assertEquals( - '12345678901234567890123456789012345678901234567890' . - '12345678901234567890123456789012345678901234567890' . - '12345678901234567890123456789012345678901234567890' . - '12345678901234567890123456789012345678901234567890' . - '12345678901234567890123456789012...', - $shortDescription, - 'Long text descriptions should be truncated appropriately' - ); - - $description = '[forum=1,6]text[/forum]56789012345678901234567890' . - '[forum=1,6]text[/forum]56789012345678901234567890' . - '[forum=1,6]text[/forum]56789012345678901234567890' . - '[forum=1,6]text[/forum]56789012345678901234567890' . - '[forum=1,6]text[/forum]56789012345678901234567890' . - '12345678901234567890123456789012345678901234567890'; - $shortDescription = $reflection->invoke($this->object, $description, $roundDescription); - $this->assertEquals( - 'text56789012345678901234567890' . - 'text56789012345678901234567890' . - 'text56789012345678901234567890' . - 'text56789012345678901234567890' . - 'text56789012345678901234567890' . - '12345678901234567890123456789012345678901234567890', - $shortDescription, - 'Markup should be removed even with no late BBCode' - ); - - $description = '[forum=1,6]text[/forum]56789012345678901234567890' . - '[forum=1,6]text[/forum]56789012345678901234567890' . - '[forum=1,6]text[/forum]56789012345678901234567890' . - '[forum=1,6]text[/forum]56789012345678901234567890' . - '1234567890[forum=1,6]text[/forum]45678901234567890' . - '12345678901234567890123456789012345678901234567890'; - $shortDescription = $reflection->invoke($this->object, $description, $roundDescription); - $this->assertEquals( - 'text56789012345678901234567890' . - 'text56789012345678901234567890' . - 'text56789012345678901234567890' . - 'text56789012345678901234567890' . - '1234567890text45678901234567890' . - '12345678901234567890123456789012345678901234567890', - $shortDescription, - 'Markup should be removed with late BBCode' - ); - } - - /** - * @covers BMInterfaceTournament::strip_nonessential_bbcode - */ - public function test_strip_nonessential_bbcode() { - $reflection = new ReflectionMethod($this->object, 'strip_nonessential_bbcode'); - $reflection->setAccessible(true); - - $text = '[button=Abe Caine] is [b][i]very[/i] annoying[/b] ' . - '[spoiler]according to [player=tasha][/spoiler], ' . - 'see [forum=1335,32790]this forum thread[/forum]'; - $strippedText = $reflection->invoke($this->object, $text); - $this->assertEquals( - '[button=Abe Caine] is very annoying ' . - '[spoiler]according to [player=tasha][/spoiler], ' . - 'see this forum thread', - $strippedText, - 'BBCode stripping should be correct' - ); - } } From e0418c019ecb054e21d385004f812669db1243a4 Mon Sep 17 00:00:00 2001 From: James Ong Date: Sat, 17 Jan 2026 18:42:44 +1100 Subject: [PATCH 10/11] Started implementing QUnit tests for TournamentOverview --- test/src/ui/js/test_TournamentOverview.js | 75 +++++++++++------------ 1 file changed, 35 insertions(+), 40 deletions(-) diff --git a/test/src/ui/js/test_TournamentOverview.js b/test/src/ui/js/test_TournamentOverview.js index 980e60bb9..6969751e4 100644 --- a/test/src/ui/js/test_TournamentOverview.js +++ b/test/src/ui/js/test_TournamentOverview.js @@ -4,13 +4,10 @@ module("TournamentOverview", { BMTestUtils.setupFakeLogin(); - // Override Env.getParameterByName to set the game - BMTestUtils.overrideGetParameterByName(); - // Create the tournament_page div so functions have something to modify - if (document.getElementById('tournamentoverview_page') == null) { + if (document.getElementById('tournament_overview_page') == null) { $('body').append($('

', {'id': 'env_message', })); - $('body').append($('
', {'id': 'tournamentoverview_page', })); + $('body').append($('
', {'id': 'tournament_overview_page', })); } // // // set colors for use in game, since tests don't always traverse showStatePage() @@ -19,7 +16,7 @@ module("TournamentOverview", { // 'opponent': '#ddffdd', // }; - Login.pageModule = { 'bodyDivId': 'tournamentoverview_page' }; + Login.pageModule = { 'bodyDivId': 'tournament_overview_page' }; }, 'teardown': function(assert) { @@ -30,10 +27,6 @@ module("TournamentOverview", { // Delete all elements we expect this module to create - // Revert cookies - Env.setCookieNoImages(false); - Env.setCookieCompactMode(false); - // JavaScript variables delete Api.new_tournaments; // delete TournamentOverview.tournament; @@ -44,13 +37,10 @@ module("TournamentOverview", { TournamentOverview.activity = {}; // Page elements - $('#tournamentoverview_page').remove(); - - BMTestUtils.restoreGetParameterByName(); + $('#tournament_overview_page').remove(); BMTestUtils.deleteEnvMessage(); BMTestUtils.cleanupFakeLogin(); - BMTestUtils.restoreGetParameterByName(); // Fail if any other elements were added or removed BMTestUtils.TournamentOverviewPost = BMTestUtils.getAllElements(); @@ -65,33 +55,38 @@ test("test_TournamentOverview_is_loaded", function(assert) { assert.ok(TournamentOverview, "The TournamentOverview namespace exists"); }); -//// The purpose of this test is to demonstrate that the flow of -//// TournamentOverview.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 +// The purpose of this test is to demonstrate that the flow of +// TournamentOverview.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_TournamentOverview.showLoggedInPage", function(assert) { -//// expect(5); -//// var cached_getCurrentTournament = Tournament.getCurrentTournament; -//// var cached_showStatePage = Tournament.showStatePage; -//// var getCurrentTournamentCalled = false; -//// Tournament.showStatePage = function() { -//// assert.ok(getCurrentTournamentCalled, "Tournament.getCurrentTournament is called before Tournament.showStatePage"); -//// }; -//// Tournament.getCurrentTournament = function(callback) { -//// getCurrentTournamentCalled = true; -//// assert.equal(callback, Tournament.showStatePage, -//// "Tournament.getCurrentTournament is called with Tournament.showStatePage as an argument"); -//// callback(); -//// }; -//// -//// Tournament.showLoggedInPage(); -//// var item = document.getElementById('tournament_page'); -//// assert.equal(item.nodeName, "DIV", -//// "#tournament_page is a div after showLoggedInPage() is called"); -//// Tournament.getCurrentTournament = cached_getCurrentTournament; -//// Tournament.showStatePage = cached_showStatePage; + expect(5); + var cached_getOverview = TournamentOverview.getOverview; + var cached_showStatePage = TournamentOverview.showStatePage; + var getOverviewCalled = false; + TournamentOverview.showPage = function() { + assert.ok( + getOverviewCalled, + "TournamentOverview.getOverview is called before TournamentOverview.showStatePage" + ); + }; + TournamentOverview.getOverview = function(callback) { + getOverviewCalled = true; + assert.equal(callback, TournamentOverview.showPage, + "TournamentOverview.getOverview is called with TournamentOverview.showPage as an argument"); + callback(); + }; + + TournamentOverview.showLoggedInPage(); + var item = document.getElementById('tournament_overview_page'); + console.log(document); + assert.equal(item.nodeName, "DIV", + "#tournament_overview_page is a div after showLoggedInPage() is called"); + + TournamentOverview.getOverview = cached_getOverview; + TournamentOverview.showPage = cached_showStatePage; }); //// Use stop()/start() because the AJAX-using operation needs to From 183f1b63e97d41a4145916bb66f3d33b1bfc6b45 Mon Sep 17 00:00:00 2001 From: James Ong Date: Sat, 17 Jan 2026 18:56:08 +1100 Subject: [PATCH 11/11] Added QUnit test for TournamentOverview.getOverview --- test/src/ui/js/test_TournamentOverview.js | 80 ++--------------------- 1 file changed, 6 insertions(+), 74 deletions(-) diff --git a/test/src/ui/js/test_TournamentOverview.js b/test/src/ui/js/test_TournamentOverview.js index 6969751e4..f22b05f4e 100644 --- a/test/src/ui/js/test_TournamentOverview.js +++ b/test/src/ui/js/test_TournamentOverview.js @@ -28,7 +28,7 @@ module("TournamentOverview", { // Delete all elements we expect this module to create // JavaScript variables - delete Api.new_tournaments; + delete Api.tournaments; // delete TournamentOverview.tournament; delete TournamentOverview.page; delete TournamentOverview.form; @@ -89,80 +89,12 @@ test("test_TournamentOverview.showLoggedInPage", function(assert) { TournamentOverview.showPage = cached_showStatePage; }); -//// Use stop()/start() because the AJAX-using operation needs to -//// finish before its results can be tested -//test("test_Tournament.redrawTournamentPageSuccess", function(assert) { -//// $.ajaxSetup({ async: false }); -//// BMTestUtils.GameType = 'frasquito_wiseman_specifydice'; -//// Tournament.redrawTournamentPageSuccess(); -//// var item = document.getElementById('tournament_page'); -//// assert.equal(item.nodeName, "DIV", -//// "#tournament_page is a div after redrawTournamentPageSuccess() is called"); -//// assert.deepEqual(Tournament.activity, {}, -//// "Tournament.activity is cleared by redrawTournamentPageSuccess()"); -//// $.ajaxSetup({ async: true }); -//}); -// -//// Use stop()/start() because the AJAX-using operation needs to -//// finish before its results can be tested -//test("test_Tournament.redrawTournamentPageFailure", function(assert) { -//// $.ajaxSetup({ async: false }); -//// BMTestUtils.GameType = 'frasquito_wiseman_specifydice'; -//// Tournament.activity.chat = "Some chat text"; -//// Tournament.redrawGamePageFailure(); -//// var item = document.getElementById('tournament_page'); -//// assert.equal(item.nodeName, "DIV", -//// "#tournament_page is a div after redrawGamePageFailure() is called"); -//// assert.equal(Tournament.activity.chat, "Some chat text", -//// "Tournament.activity.chat is retained by redrawTournamentPageSuccess()"); -//// $.ajaxSetup({ async: true }); -//}); -// -//// N.B. Almost all of these tests should use stop(), set a test -//// game type, and invoke Tournament.getCurrentTournament(), because that's the -//// way to get the dummy responder data which all the other functions -//// need. Then run tests against the function itself, and end with -//// start(). So the typical format will be: -//// -//// test("test_Tournament.someFunction", function(assert) { -//// stop(); -//// BMTestUtils.GameType = ''; -//// Tournament.getCurrentTournament(function() { -//// -//// Tournament.someFunction(); -//// -//// start(); -//// }); -//// }); -// -//test("test_Tournament.getCurrentTournament", function(assert) { -//// stop(); -//// BMTestUtils.GameType = 'frasquito_wiseman_specifydice'; -//// var gameId = BMTestUtils.testGameId(BMTestUtils.GameType); -//// Tournament.getCurrentTournament(function() { -//// assert.equal(Tournament.tournament, gameId, "Set expected game number"); -//// assert.equal(Api.tournament.load_status, 'ok', 'Successfully loaded game data'); -//// assert.equal(Api.tournament.gameId, Tournament.tournament, 'Parsed correct game number from API'); -//// start(); -//// }); -//}); -// -//test("test_Tournament.showStatePage", function(assert) { -//// stop(); -//// BMTestUtils.GameType = 'frasquito_wiseman_specifydice'; -//// Tournament.getCurrentTournament(function() { -//// Tournament.showStatePage(); -//// var htmlout = Tournament.page.html(); -//// assert.ok(htmlout.length > 0, -//// "The created page should have nonzero contents"); -//// assert.ok(htmlout.match('vacation16.png'), -//// "The game UI contains a vacation icon when the API data reports that one player is on vacation"); -//// start(); -//// }); -//}); - test("test_TournamentOverview.getOverview", function(assert) { - + stop(); + TournamentOverview.getOverview(function() { + assert.ok(Api.tournaments, "tournaments are parsed from server"); + start(); + }); }); test("test_TournamentOverview.showPage", function(assert) {