import { TestCase } from "./framework/test_case.js"; import { setupDom, teardownDom } from "./framework/dom.js"; import { CityModal, NationModal, ConfirmModal, NewsModal } from "../client/js/modals.js"; import { buildingEffectText, describeNews } from "../client/js/modals/format.js"; import { BUILDINGS, GOVERNMENTS, TECHNOLOGIES, PROTO_UNITS, RESOURCE_RULES, technologyCost, TREATY_PEACE, TREATY_ALLIANCE, TREATY_GIVE_MONEY } from "../shared/data.js"; import { groupDigits } from "../shared/text_format.js"; import { setCurrency, setCurrencyBook } from "../client/js/currency.js"; import { formatResourceAmount, protoUpkeep, unitFoodPerDay } from "../shared/resources.js"; export class CityModalTest extends TestCase { test_utility_buildings_describe_their_mechanic() { const barracks = BUILDINGS.find((b) => b.id === "barracks"); this.assertTrue( buildingEffectText(barracks, 2).includes("3 parallel training lines"), "the barracks explains its parallel lines" ); const shelters = BUILDINGS.find((b) => b.id === "shelters"); this.assertTrue(buildingEffectText(shelters, 2).includes("20%"), "the shelters show their protection"); const airport = BUILDINGS.find((b) => b.id === "airport"); this.assertTrue(buildingEffectText(airport, 1).includes("production"), "stats keep their effect text"); } async test_research_buildings_hover_with_the_erlenmeyer_icon() { const env = await setupDom(); try { const modal = new CityModal(); modal.show({ id: 1, name: "Lyon" }, { budget: 1.0e15, buildings: BUILDINGS, levels: {} }); const index = BUILDINGS.findIndex((b) => b.id === "research_lab"); const card = env.$("#city-buildings-list .building-tile").eq(index); card.find(".bt-upgrade").trigger("mouseenter"); const $tip = env.$(".card-tooltip"); this.assertEqual( $tip.find("img[src$='icon_research.svg']").length, 1, "the lab's science output carries the erlenmeyer icon" ); this.assertFalse($tip.text().includes("research"), "the unit word is replaced by the icon"); modal.close(); } finally { teardownDom(env); } } showTrain(modal, queue = []) { modal.show( { id: 1, name: "Lyon" }, { budget: 1.0e9, gdp: 1.0e9, trainable: [{ ...PROTO_UNITS[0], serverIndex: 0 }, { ...PROTO_UNITS[2], serverIndex: 2 }], unitProtos: PROTO_UNITS, buildings: BUILDINGS, levels: {}, queue, } ); } async test_train_tab_lists_trainable_units_and_requests_one() { const env = await setupDom(); try { const modal = new CityModal(); let requested = null; modal.onTrain = (cityId, protoIndex) => { requested = [cityId, protoIndex]; }; this.showTrain(modal); this.assertEqual(env.$("#city-title").text(), "Lyon — City"); this.assertSize(env.$("#city-train-list button"), 2, "infantry and artillery can be trained"); this.assertTrue( env.$("#city-train-list .bt-name").first().text().includes("Modern infantry"), "the unit is named on its card" ); const upkeep = protoUpkeep(PROTO_UNITS[0], PROTO_UNITS[0].cost); const note = env.$("#city-train-list .bt-note").first().text(); this.assertFalse(note.includes("Upkeep"), "the note just shows the resources"); this.assertTrue( note.includes(formatResourceAmount("steel", upkeep.steel)), "the unit's steel upkeep is on its card" ); this.assertTrue( note.includes(formatResourceAmount("food", unitFoodPerDay(PROTO_UNITS[0]))), "the unit's food upkeep is on its card" ); this.assertFalse(note.includes("MWh"), "energy is not billed to the unit"); this.assertFalse(note.includes("€") || note.includes("¤"), "no money upkeep is shown"); env.$("#city-train-list button").first().click(); this.assertEqual(requested, [1, 0]); modal.close(); this.assertTrue(env.$("#modal-city").hasClass("hidden")); } finally { teardownDom(env); } } async test_training_progress_states() { const env = await setupDom(); try { const modal = new CityModal(); this.showTrain(modal); modal.beginTraining("Modern infantry", 10, 2); this.assertFalse(env.$("#city-train-status").hasClass("hidden")); modal.setTrainingProgress("Modern infantry", 4, 10); this.assertTrue(env.$("#city-train-label").text().includes("6 h left")); modal.setTrainingProgress("Market", 1, 5, "Building"); this.assertTrue(env.$("#city-train-label").text().includes("Building Market")); modal.endTraining(); this.assertEqual(env.$("#city-train-label").text(), "Training complete"); } finally { teardownDom(env); } } async test_queue_labels_building_entries_alongside_units() { const env = await setupDom(); try { const modal = new CityModal(); const queue = [ { kind: "building", protoIndex: 0, elapsedHours: 0, totalHours: 5 }, { protoIndex: 0, elapsedHours: 0, totalHours: 10 }, ]; this.showTrain(modal, queue); const rows = env.$("#city-train-queue .queue-row"); this.assertSize(rows, 2, "a building and a unit never merge"); this.assertTrue( rows.eq(0).text().includes(BUILDINGS[0].name), "the building entry is labelled with its name" ); this.assertTrue(rows.eq(1).text().includes("Modern infantry"), "the unit keeps its name"); } finally { teardownDom(env); } } async test_queue_merges_only_contiguous_identical_units() { const env = await setupDom(); try { const modal = new CityModal(); const cancelled = []; modal.onCancel = (cityId, index) => cancelled.push([cityId, index]); const queue = [ { protoIndex: 0, elapsedHours: 0, totalHours: 10 }, { protoIndex: 0, elapsedHours: 0, totalHours: 10 }, { protoIndex: 2, elapsedHours: 0, totalHours: 30 }, { protoIndex: 0, elapsedHours: 0, totalHours: 10 }, ]; this.showTrain(modal, queue); const rows = env.$("#city-train-queue .queue-row"); this.assertSize(rows, 3, "only adjacent runs share a row"); this.assertTrue(rows.eq(0).text().includes("Modern infantry ×2"), "the run is labelled with its size"); this.assertTrue(rows.eq(0).text().includes("Next in"), "a run shows when its first unit lands"); this.assertTrue(rows.eq(0).text().includes("all ready in"), "and when the run completes"); this.assertTrue(rows.eq(1).text().includes("Artillery"), "a different type starts a row"); this.assertTrue(rows.eq(2).text().includes("Modern infantry"), "a later run of the same type is separate"); this.assertFalse(rows.eq(2).text().includes("×"), "the lone trailing unit is not a run"); rows.eq(0).find("button").click(); this.assertEqual(cancelled, [[1, 1]], "cancelling a run removes its last unit"); } finally { teardownDom(env); } } async test_clicking_a_gathering_order_shows_its_materials() { const env = await setupDom(); try { const modal = new CityModal(); const queue = [ { kind: "unit", protoIndex: 0, phase: "materials", elapsedHours: 0, totalHours: 10, needed: { steel: 100, hightech: 4 }, bought: { steel: 25, hightech: 0 }, budget: 123456, }, ]; this.showTrain(modal, queue); const row = env.$("#city-train-queue .queue-row").first(); this.assertTrue(row.text().includes("Gathering materials"), "the queue says it is gathering"); this.assertSize(env.$("#city-train-queue .queue-detail"), 0, "the detail is closed to start"); row.click(); this.assertSize(env.$("#city-train-queue .queue-detail"), 1, "clicking a queued unit opens its detail"); this.assertTrue(env.$("#city-train-queue .queue-detail").text().includes("Steel"), "the detail names the steel it gathers"); this.assertTrue(env.$("#city-train-queue .queue-detail").text().includes("High-tech"), "and the high-tech"); this.assertTrue( env.$("#city-train-queue .queue-detail").text().includes(groupDigits(RESOURCE_RULES.unitPopulation)), "and the soldiers the region will give up" ); // The row is rebuilt on toggle, so click the new node to close it. env.$("#city-train-queue .queue-row").first().click(); this.assertSize(env.$("#city-train-queue .queue-detail"), 0, "clicking again closes it"); } finally { teardownDom(env); } } async test_buildings_tab_prices_rows_and_requests_build_and_demolish() { const env = await setupDom(); try { const modal = new CityModal(); let built = null; let demolished = null; modal.onBuild = (cityId, index) => { built = [cityId, index]; }; modal.onDemolish = (cityId, index) => { demolished = [cityId, index]; }; modal.show({ id: 3, name: "Lyon" }, { budget: 1.0e15, buildings: BUILDINGS, levels: {} }); this.assertSize(env.$("#city-buildings-list .building-tile"), BUILDINGS.length); const first = env.$("#city-buildings-list .building-tile").first(); this.assertTrue(first.find(".bt-level").text().includes("Level 0")); this.assertFalse(first.find(".bt-upgrade").prop("disabled")); first.find(".bt-upgrade").click(); this.assertEqual(built, [3, 0]); modal.refreshBuildings(1.0e15, { 0: 1 }); this.assertTrue(first.find(".bt-level").text().includes("Level 1")); first.find(".bt-downgrade").click(); this.assertEqual(demolished, [3, 0]); modal.close(); this.assertTrue(env.$("#modal-city").hasClass("hidden")); } finally { teardownDom(env); } } async test_queued_levels_price_the_next_upgrade_higher() { const env = await setupDom(); try { const modal = new CityModal(); modal.show({ id: 1, name: "X" }, { budget: 1.0e15, buildings: BUILDINGS, levels: {}, gdp: 1.0e9 }); const first = env.$("#city-buildings-list .building-tile").first(); const plain = first.find(".bt-meta").text(); modal.refreshBuildings(1.0e15, {}, 1.0e9, { 0: 1 }); const queued = first.find(".bt-meta").text(); this.assertTrue(first.find(".bt-level").text().includes("queued"), "the pending level is shown"); this.assertNotEqual(queued, plain, "the next order is priced off the queued level"); } finally { teardownDom(env); } } async test_upgrade_stays_available_on_an_empty_treasury() { const env = await setupDom(); try { const modal = new CityModal(); modal.show({ id: 1, name: "X" }, { budget: 0, buildings: BUILDINGS, levels: {} }); const upgrade = env.$("#city-buildings-list .building-tile").first().find(".bt-upgrade"); // The government can always pay, running an overdraft, so cost never // disables the button; only an unmet requirement does. this.assertFalse(upgrade.prop("disabled")); } finally { teardownDom(env); } } async test_budget_tab_renders_region_income_and_upkeep() { const env = await setupDom(); try { const modal = new CityModal(); const breakdown = { income: { rate: 0.001, base: 900, modifier: 0.15, total: 1035, sources: [ { label: "Bank", kind: "building", effect: 0.05, amount: 45, cityId: 1, cityName: "Paris" }, ], }, buildingUpkeep: { total: 400, sources: [{ label: "Bank", kind: "building", amount: 400 }], groups: [ { index: 1, label: "Bank", amount: 400, cities: [{ cityId: 1, cityName: "Paris", level: 2, amount: 400 }], }, ], }, unitUpkeep: { total: 0, sources: [], groups: [] }, net: 635, }; modal.show({ id: 1, name: "Paris" }, { budget: 1.0e15, buildings: BUILDINGS, levels: {} }); modal.renderBudget(breakdown); this.assertTrue(env.$("#city-budget-sub").text().includes("+¤635/h"), "the region net is shown"); const text = env.$("#city-budget-list").text(); this.assertTrue(text.includes("+¤1 035/h"), "region tax income is green"); this.assertTrue(text.includes("Bank in Paris (building) +5.0%"), "a building bonus is listed"); this.assertTrue(text.includes("−¤400/h"), "the building upkeep is listed"); this.assertTrue(text.includes("TOTAL"), "a total row is present"); this.assertTrue(text.includes("+¤635/h"), "the total carries the region net"); this.assertFalse(text.includes("No units"), "unit upkeep is national, not regional"); env.$("#city-budget-list tr.budget-group .budget-name").click(); this.assertTrue( env.$("#city-budget-list").text().includes("Bank in Paris — level 2"), "expanding a building type shows its level" ); } finally { teardownDom(env); } } async test_the_people_tab_warns_of_famine() { const env = await setupDom(); try { const modal = new CityModal(); modal.show({ id: 1, name: "Paris" }, { budget: 1.0e15, buildings: BUILDINGS, levels: {}, ethnicMakeup: [], famine: true, }); this.assertTrue( env.$("#city-people-list").text().includes("Famine"), "the famine warning is shown" ); } finally { teardownDom(env); } } async test_tabs_switch_the_visible_page() { const env = await setupDom(); try { const modal = new CityModal(); this.showTrain(modal); this.assertTrue(env.$("#city-tab-train").hasClass("active")); env.$("#modal-city .tab[data-tab=buildings]").click(); this.assertTrue(env.$("#city-tab-buildings").hasClass("active")); this.assertFalse(env.$("#city-tab-train").hasClass("active")); env.$("#modal-city .tab[data-tab=budget]").click(); this.assertTrue(env.$("#city-tab-budget").hasClass("active")); env.$("#modal-city .tab[data-tab=train]").click(); this.assertTrue(env.$("#city-tab-train").hasClass("active")); } finally { teardownDom(env); } } async test_economy_tab_shows_the_regions_cash_basket() { const env = await setupDom(); try { const modal = new CityModal(); modal.show( { id: 1, name: "Lyon" }, { budget: 1.0e9, gdp: 1.0e9, cash: { FRF: 4000, GBP: 250 }, cashValue: 4250, debt: 250, currencies: [ { code: "FRF", symbol: "₣", name: "French Franc" }, { code: "GBP", symbol: "£", name: "British Pound" }, ], } ); env.$("#modal-city .tab[data-tab=economy]").click(); this.assertTrue(env.$("#city-tab-economy").hasClass("active")); const text = env.$("#city-economy-list").text(); this.assertTrue(text.includes("French Franc"), "the national currency is listed"); this.assertTrue(text.includes("British Pound"), "a foreign holding is listed"); this.assertTrue(env.$("#city-economy-sub").text().includes("4 250"), "the total value shows"); this.assertTrue(env.$("#city-economy-sub").text().includes("Debt"), "the central-bank debt shows"); } finally { teardownDom(env); } } async test_resources_tab_shows_reserves_value_and_consumption() { const env = await setupDom(); try { const modal = new CityModal(); modal.show( { id: 1, name: "Lyon" }, { budget: 1.0e9, gdp: 1.0e9, resources: { stock: { food: 1000, steel: 2000, luxury: 0, hightech: 50 }, consumption: { food: 100, steel: 200, luxury: 10, hightech: 2, energy: 2500 }, value: 123456, }, } ); env.$("#modal-city .tab[data-tab=resources]").click(); this.assertTrue(env.$("#city-tab-resources").hasClass("active")); this.assertTrue( env.$("#city-resources-sub").text().includes("123 456"), "the reserve value shows" ); const text = env.$("#city-resources-list").text(); this.assertTrue(text.includes("Food"), "food is listed"); this.assertTrue(text.includes("Steel"), "steel is listed"); this.assertTrue(text.includes("Energy"), "energy is listed"); this.assertTrue(text.includes("2.5 MWh"), "the daily power draw is shown"); } finally { teardownDom(env); } } async test_people_tab_reports_resource_shortages() { const env = await setupDom(); try { const modal = new CityModal(); modal.show( { id: 1, name: "Lyon" }, { budget: 1.0e9, gdp: 1.0e9, ethnicMakeup: [], approval: 0.5, shortage: { steel: { fraction: 0.4, amount: 1200 } }, } ); env.$("#modal-city .tab[data-tab=people]").click(); this.assertTrue(env.$("#city-tab-people").hasClass("active")); const text = env.$("#city-people-list").text(); this.assertTrue(text.includes("Resource shortages"), "the shortage section shows"); this.assertTrue(text.includes("Steel"), "the short resource is named"); this.assertTrue(text.includes("1\u00a0200"), "the amount it is short by is shown"); this.assertTrue(text.includes("40%"), "the share of need is shown"); } finally { teardownDom(env); } } } export class NationModalTest extends TestCase { async test_government_and_technology_tabs() { const env = await setupDom(); try { const modal = new NationModal(); let government = null; let technology = null; modal.onGovernment = (index) => { government = index; }; modal.onFocusTechnology = (index) => { technology = index; }; modal.show(GOVERNMENTS, 0, TECHNOLOGIES, new Set(), 1.0e9, 1.0e15, 1.0e6); this.assertSize(env.$("#government-list .row-card"), GOVERNMENTS.length); this.assertSize(env.$("#technology-list .building-tile"), TECHNOLOGIES.length); const governmentRows = env.$("#government-list .row-card"); this.assertTrue(governmentRows.eq(0).find(".actions button").prop("disabled"), "current government is locked"); governmentRows.eq(1).find(".actions button").click(); this.assertEqual(government, 1); // A prerequisite-gated technology starts locked. const method = TECHNOLOGIES.findIndex((t) => t.id === "scientific_method"); const automation = TECHNOLOGIES.findIndex((t) => t.id === "industrial_automation"); const techRows = env.$("#technology-list .building-tile"); this.assertEqual(techRows.eq(automation).find(".bt-upgrade").text(), "Locked"); // Researching its prerequisite unlocks it. modal.refresh(0, new Set([method]), 1.0e9, 1.0e15, 1.0e6); this.assertTrue(techRows.eq(method).find(".bt-upgrade").text().startsWith("Researched")); this.assertTrue(techRows.eq(automation).find(".bt-upgrade").text().startsWith("Focus")); techRows.eq(automation).find(".bt-upgrade").click(); this.assertEqual(technology, automation); env.$("#modal-nation .tab[data-tab=technology]").click(); this.assertTrue(env.$("#tab-technology").hasClass("active")); env.$("#modal-nation .tab[data-tab=government]").click(); this.assertTrue(env.$("#tab-government").hasClass("active")); modal.close(); this.assertTrue(env.$("#modal-nation").hasClass("hidden")); } finally { teardownDom(env); } } async test_technology_theme_tabs_filter_the_list() { const env = await setupDom(); try { const modal = new NationModal(); modal.show(GOVERNMENTS, 0, TECHNOLOGIES, new Set(), 1.0e9, 1.0e15, 1.0e6); const $themes = env.$("#technology-themes .subtab"); this.assertGreater($themes.length, 1, "the technologies are split into themes"); const method = TECHNOLOGIES.findIndex((t) => t.id === "scientific_method"); const automation = TECHNOLOGIES.findIndex((t) => t.id === "industrial_automation"); const rows = env.$("#technology-list .building-tile"); this.assertNotEqual(rows.eq(method).css("display"), "none", "science is the opening theme"); this.assertEqual(rows.eq(automation).css("display"), "none", "other themes start hidden"); $themes.filter((_index, element) => env.$(element).text() === "Economy").trigger("click"); this.assertEqual(rows.eq(method).css("display"), "none", "science is hidden"); this.assertNotEqual(rows.eq(automation).css("display"), "none", "the economy theme shows"); modal.close(); } finally { teardownDom(env); } } async test_a_repeatable_technology_shows_its_level() { const env = await setupDom(); try { const modal = new NationModal(); const method = TECHNOLOGIES.findIndex((t) => t.id === "scientific_method"); modal.show(GOVERNMENTS, 0, TECHNOLOGIES, new Set([method]), 1.0e9, 1.0e15, 1.0e6, {}, null, [], null, { focus: -1, progress: 0, repeatCounts: { infantry_weapons: 3 }, }); const index = TECHNOLOGIES.findIndex((t) => t.id === "infantry_weapons"); const card = env.$("#technology-list .building-tile").eq(index); this.assertTrue(card.find(".bt-level").text().includes("Level 3"), "the level is shown"); const $meta = card.find(".bt-meta"); this.assertEqual( $meta.find("img[src$='icon_research.svg']").length, 1, "the erlenmeyer icon stands in for the research unit" ); this.assertTrue($meta.text().length > 0, `the next level is priced: ${$meta.text()}`); this.assertFalse($meta.text().includes("RP"), "no text 'RP' unit is left"); modal.close(); } finally { teardownDom(env); } } async test_resources_tab_lists_stores_needs_and_the_market() { const env = await setupDom(); try { const modal = new NationModal(); const summary = { stock: { steel: 1000, food: 2_000_000, luxury: 5000, hightech: 12 }, need: { steel: 10, food: 20, luxury: 30, hightech: 0 }, production: { steel: 5, food: 0, luxury: 0, hightech: 1 }, consumed: { steel: 15, food: 25, luxury: 35, hightech: 2, energy: 1.1e9 }, energy: { need: 1.0e9, supply: 2.0e9 }, trade: { imports: { steel: 40 }, exports: { steel: 7 } }, prices: {}, }; const market = [ { id: "energy", price: 0.003, supply: 1.0e9, demand: 2.0e9 }, { id: "steel", price: 600, supply: 100, demand: 200 }, { id: "food", price: 300, supply: 50, demand: 60 }, { id: "luxury", price: 60, supply: 0, demand: 10 }, { id: "hightech", price: 800, supply: 0, demand: 0 }, ]; modal.show( GOVERNMENTS, 0, TECHNOLOGIES, new Set(), 1.0e9, 1.0e15, 5, {}, null, [], null, null, { summary, market } ); env.$("#modal-nation .tab[data-tab=resources]").click(); this.assertTrue(env.$("#tab-resources").hasClass("active"), "the tab opens"); const rows = env.$("#resources-list .resources-table tbody tr"); this.assertSize(rows, 5, "one row for every resource"); const energy = rows.eq(0); this.assertTrue(energy.find(".name").text().includes("Energy"), "energy leads the list"); this.assertEqual(energy.find("td").eq(1).text(), "—", "energy has no store"); const steel = rows.eq(1); this.assertTrue(steel.find(".name").text().includes("Steel")); this.assertTrue(steel.find("td").eq(2).text().includes("15"), "consumption is shown"); this.assertTrue(steel.find("td").eq(4).text().includes("7"), "exports are shown"); this.assertTrue(steel.find("td").eq(5).text().includes("40"), "imports are shown"); this.assertTrue(steel.find("td").eq(6).text().includes("600"), "the steel price is shown"); this.assertTrue(steel.find("td").eq(7).text().length > 0, "world supply is shown"); modal.refresh( 0, new Set(), 1.0e9, 1.0e15, 5, {}, null, null, null, null, { summary, market } ); this.assertSize(env.$("#resources-list .resources-table tbody tr"), 5, "refreshing keeps the rows"); modal.close(); } finally { teardownDom(env); } } async test_resources_tab_ranks_major_exporters_and_importers() { const env = await setupDom(); try { const modal = new NationModal(); const summary = { stock: { steel: 0, food: 0, luxury: 0, hightech: 0 }, need: { steel: 0, food: 0, luxury: 0, hightech: 0 }, production: { steel: 0, food: 0, luxury: 0, hightech: 0 }, consumed: { steel: 0, food: 0, luxury: 0, hightech: 0, energy: 0 }, energy: { need: 0, supply: 0 }, trade: { imports: {}, exports: {} }, prices: {}, }; const trade = [ { civ: 0, imports: { steel: 10 }, exports: { steel: 100 } }, { civ: 1, imports: { steel: 50 }, exports: { steel: 5 } }, ]; const civs = [{ name: "France" }, { name: "Britain" }]; modal.show( GOVERNMENTS, 0, TECHNOLOGIES, new Set(), 1.0e9, 1.0e15, 5, {}, null, [], null, null, { summary, market: [], trade, civs } ); env.$("#modal-nation .tab[data-tab=resources]").click(); env.$("#resources-subtabs .subtab[data-view=leaders]").click(); const rows = env.$("#resource-trade-list .resources-table tbody tr"); this.assertSize(rows, 5, "one row for every resource"); const steel = rows.eq(1); this.assertTrue(steel.find("td").eq(1).text().includes("France"), "the major exporter leads"); this.assertTrue(steel.find("td").eq(3).text().includes("Britain"), "the major importer leads"); modal.close(); } finally { teardownDom(env); } } async test_a_focused_technology_shows_a_progress_bar() { const env = await setupDom(); try { const modal = new NationModal(); const method = TECHNOLOGIES.findIndex((t) => t.id === "scientific_method"); modal.show(GOVERNMENTS, 0, TECHNOLOGIES, new Set(), 1.0e9, 1.0e15, 5, {}, null, [], null, { focus: method, progress: 10, repeatCounts: {}, }); const card = env.$("#technology-list .building-tile").eq(method); this.assertTrue(card.find(".bt-upgrade .bt-progress-fill").length > 0, "the button is the bar"); this.assertNotEqual(card.find(".bt-upgrade .bt-progress-fill").css("width"), "0%", "the bar is filled"); this.assertEqual( card.find(".bt-meta").text(), `10 / ${groupDigits(technologyCost(TECHNOLOGIES[method]))}`, "X / Y beside it" ); this.assertEqual(card.find(".bt-up-label").text(), "Focused", "no cost on the button"); modal.close(); } finally { teardownDom(env); } } async test_military_technology_effects_are_readable_text() { const env = await setupDom(); try { const modal = new NationModal(); modal.show(GOVERNMENTS, 0, TECHNOLOGIES, new Set(), 1.0e9, 1.0e15, 5); const infantry = TECHNOLOGIES.findIndex((t) => t.id === "infantry_weapons"); env.$("#technology-list .building-tile").eq(infantry).find(".bt-upgrade").trigger("mouseenter"); this.assertTrue( env.$(".card-tooltip").text().includes("+1.0% infantry attack"), "the effect is in the hover detail" ); modal.close(); } finally { teardownDom(env); } } async test_the_research_rate_reads_with_the_erlenmeyer_icon() { const env = await setupDom(); try { const modal = new NationModal(); modal.show(GOVERNMENTS, 0, TECHNOLOGIES, new Set(), 1.0e9, 1.0e15, 2.5); const $sub = env.$("#technology-sub"); this.assertEqual( $sub.find("img[src$='icon_research.svg']").length, 1, "the rate carries the erlenmeyer icon" ); this.assertTrue($sub.text().includes("2.5"), "the rate is shown"); this.assertFalse($sub.text().includes("RP"), "no text 'RP' unit is left"); modal.close(); } finally { teardownDom(env); } } async test_reopening_rebuilds_rows_instead_of_stacking_them() { const env = await setupDom(); try { const modal = new NationModal(); let technology = null; modal.onFocusTechnology = (index) => { technology = index; }; modal.show(GOVERNMENTS, 0, TECHNOLOGIES, new Set(), 1.0e9, 1.0e15, 0); modal.close(); // Reopen with enough research to afford the first technology. modal.show(GOVERNMENTS, 0, TECHNOLOGIES, new Set(), 1.0e9, 1.0e15, 1.0e6); this.assertSize(env.$("#government-list .row-card"), GOVERNMENTS.length, "no stacked governments"); this.assertSize(env.$("#technology-list .building-tile"), TECHNOLOGIES.length, "no stacked technologies"); const button = env.$("#technology-list .building-tile").first().find(".bt-upgrade"); this.assertFalse(button.prop("disabled"), "the rebuilt button reflects the new budget"); button.click(); this.assertEqual(technology, 0); } finally { teardownDom(env); } } async test_budget_tab_shows_monthly_income_and_expenses() { const env = await setupDom(); try { const modal = new NationModal(); const month = (overrides) => { const income = overrides.income || 0; return { monthIndex: 24000, isCurrent: true, label: "January 2000", income, taxes: { sales: income, export: 0, import: 0 }, sales: 0, totalCash: 0, totalEconomic: 0, net: 0, expenses: [], resourceTrade: [], ...overrides, }; }; const finances = { months: [ month({ monthIndex: 23999, isCurrent: false, label: "December 1999", income: 500, totalCash: 400, totalEconomic: 1200, net: 100, expenses: [["buildings", 400, 1200]], }), month({ income: 1035, sales: 60, totalCash: 550, totalEconomic: 900, net: 545, expenses: [ ["buildings", 400, 700, [ ["steel", 250, [["Barracks", 250]]], ["hightech", 150, [["Laboratory", 150]]], ]], ["units", 150, 200, []], ], resourceTrade: [["steel", 220, 0], ["food", 0, 90]], }), ], cities: [], }; modal.show(GOVERNMENTS, 0, TECHNOLOGIES, new Set(), 1.0e9, 1.0e15, 1.0e6, finances); env.$("#modal-nation .tab[data-tab=budget]").click(); this.assertTrue(env.$("#tab-budget").hasClass("active")); this.assertSize(env.$("#budget-list table.budget-table"), 1); const text = env.$("#budget-list").text(); this.assertTrue(text.includes("National sales tax"), "the month's taxes are shown"); this.assertTrue(text.includes("+¤1 035"), "the tax take is shown"); this.assertTrue(text.includes("Resource sales"), "resource sales are shown"); this.assertTrue(text.includes("Building upkeep"), "a building category is listed"); this.assertTrue(text.includes("−¤700"), "the rightmost column is the market value"); this.assertTrue(text.includes("cash −¤400"), "the cash the treasury paid is noted"); this.assertTrue(text.includes("Unit upkeep"), "a unit category is listed"); this.assertTrue(text.includes("Bought Steel"), "a bought resource is listed"); this.assertTrue(text.includes("Sold Food"), "a sold resource is listed"); this.assertTrue(text.includes("TOTAL"), "a total row is present"); this.assertTrue(text.includes("+¤545"), "the total carries the month net"); this.assertTrue(env.$("#budget-sub").text().includes("Treasury")); this.assertTrue(env.$("#budget-sub").text().includes("January 2000"), "the month is named"); // Expanding an upkeep row names the resources it bought and the buildings // that consumed them. env.$("#budget-list tr .budget-name.clickable").first().click(); const expanded = env.$("#budget-list").text(); this.assertTrue(expanded.includes("Steel"), "the expanded row names steel"); this.assertTrue(expanded.includes("−¤250"), "and what it paid for it"); this.assertTrue(expanded.includes("Barracks"), "and the building that used it"); // The pager steps back to the previous month's digest. env.$("#budget-months .budget-month-nav").first().click(); this.assertTrue( env.$("#budget-sub").text().includes("December 1999"), "the pager names the older month" ); modal.refresh(0, new Set(), 1.0e9, 1.0e15, 1.0e6, { months: [month({ income: 100, totalCash: 400, totalEconomic: 400, net: -300, expenses: [["buildings", 400, 400]], })], cities: [], }); const refreshed = env.$("#budget-list").text(); this.assertTrue(refreshed.includes("−¤300"), "the total updates"); } finally { teardownDom(env); } } async test_cities_tab_lists_regions_with_their_tax() { const env = await setupDom(); try { const modal = new NationModal(); const finances = { income: { rate: 0.001, base: 0, modifier: 0, total: 0, sources: [], regions: [] }, buildingUpkeep: { total: 0, sources: [], groups: [] }, unitUpkeep: { total: 0, sources: [], groups: [] }, net: 0, cities: [ { id: 1, civ: 0, name: "Paris", isCapital: true, population: 1.0e6, gdp: 1.0e10, income: 300 }, { id: 2, civ: 0, name: "Lille", isCapital: false, population: 5.0e5, gdp: 4.0e9, income: 120 }, { id: 3, civ: 1, name: "London", isCapital: true, population: 9.0e5, gdp: 8.0e9, income: 240 }, ], }; modal.show(GOVERNMENTS, 0, TECHNOLOGIES, new Set(), 1.0e9, 1.0e15, 1.0e6, finances, { civs: [{ name: "France" }, { name: "Britain" }], localCiv: 0, relations: [], stats: [], }); env.$("#modal-nation .tab[data-tab=cities]").click(); this.assertTrue(env.$("#tab-cities").hasClass("active")); const text = env.$("#cities-list").text(); this.assertTrue(text.includes("Paris"), "own regions are listed"); this.assertTrue(text.includes("Lille"), "every own region is listed"); this.assertFalse(text.includes("London"), "foreign regions are hidden"); this.assertTrue(text.includes("TOTAL"), "the region totals are summed"); } finally { teardownDom(env); } } async test_diplomacy_tab_lists_nations_and_declares_war() { const env = await setupDom(); try { const modal = new NationModal(); let declared = null; modal.onDeclareWar = (index) => { declared = index; }; const civs = [{ name: "France" }, { name: "Britain" }, { name: "Poland" }]; const relations = [[0, 1, "peace"], [0, 2, "war"], [1, 2, "peace"]]; const stats = [ { population: 2.0e6, gdp: 5.0e9, gdpPerCapita: 2500 }, { population: 1.5e6, gdp: 3.0e9, gdpPerCapita: 2000 }, { population: 1.0e6, gdp: 1.5e9, gdpPerCapita: 1500 }, ]; modal.show( GOVERNMENTS, 0, TECHNOLOGIES, new Set(), 1.0e9, 1.0e15, 1.0e6, {}, { civs, localCiv: 0, relations, stats } ); env.$("#modal-nation .tab[data-tab=diplomacy]").click(); this.assertTrue(env.$("#tab-diplomacy").hasClass("active")); const rows = env.$("#diplomacy-list tbody tr"); this.assertSize(rows, 3, "every nation, including ours, is listed"); this.assertTrue(rows.eq(0).find(".name").text() === "Britain", "alphabetical by default"); this.assertTrue(rows.eq(0).find(".status").text().includes("At peace")); const numbers = rows.eq(0).find("td.numeric"); this.assertTrue(numbers.eq(0).text().includes("¤3B"), "Britain's GDP shows"); this.assertTrue(numbers.eq(1).text().includes("¤2K"), "Britain's GDP/capita shows"); this.assertTrue(numbers.eq(2).text().includes("1.5M"), "Britain's population shows"); const own = rows.eq(1); this.assertTrue(own.find(".name").text() === "France", "our own nation is listed"); this.assertTrue(own.hasClass("local-nation"), "our row is highlighted"); this.assertTrue(own.find(".status").text().includes("Your nation")); this.assertSize(own.find("button"), 0, "we cannot declare war on ourselves"); this.assertTrue(rows.eq(2).find(".name").text() === "Poland"); this.assertTrue(rows.eq(2).find(".status").text().includes("At war")); this.assertTrue(rows.eq(2).find("button").prop("disabled"), "already at war"); this.assertTrue(env.$("#diplomacy-sub").text().includes("at war with 1")); rows.eq(0).find("button").click(); this.assertEqual(declared, 1); modal.close(); this.assertTrue(env.$("#modal-nation").hasClass("hidden")); } finally { teardownDom(env); } } async test_diplomacy_tab_names_each_nations_controller() { const env = await setupDom(); try { const modal = new NationModal(); const civs = [{ name: "France" }, { name: "Britain" }, { name: "Poland" }]; const stats = [ { population: 1.0e6, gdp: 1.0e9, gdpPerCapita: 1000 }, { population: 1.0e6, gdp: 1.0e9, gdpPerCapita: 1000 }, { population: 1.0e6, gdp: 1.0e9, gdpPerCapita: 1000 }, ]; const players = { 1: { id: 1, name: "Alice", admin: true, civ: 0 }, 2: { id: 2, name: "Bob", admin: false, civ: 1 }, }; modal.show(GOVERNMENTS, 0, TECHNOLOGIES, new Set(), 1.0e9, 1.0e15, 1.0e6, {}, { civs, localCiv: 0, relations: [], stats, players, }); env.$("#modal-nation .tab[data-tab=diplomacy]").click(); const byName = (name) => env.$("#diplomacy-list tbody tr").filter( (_, el) => env.$(el).find(".name").text() === name ).first(); const britain = byName("Britain"); this.assertTrue( britain.find("td.control img").attr("src").includes("icon_human"), "a player-run nation shows the human icon" ); this.assertEqual(britain.find("td.leader").text(), "Bob", "the leader's name shows"); const poland = byName("Poland"); this.assertTrue( poland.find("td.control img").attr("src").includes("icon_computer"), "an unclaimed nation shows the computer icon" ); this.assertEqual(poland.find("td.leader").text(), "—", "it has no leader name"); modal.close(); } finally { teardownDom(env); } } async test_diplomacy_tab_proposes_answers_and_breaks_treaties() { const env = await setupDom(); try { const modal = new NationModal(); const civs = [{ name: "France" }, { name: "Britain" }]; const stats = [ { population: 1.0e6, gdp: 1.0e9, gdpPerCapita: 1000 }, { population: 1.0e6, gdp: 1.0e9, gdpPerCapita: 1000 }, ]; const info = { civs, localCiv: 0, relations: [], stats, players: {}, treaties: [{ id: 3, type: TREATY_ALLIANCE, a: 0, b: 1, payload: {}, signedHours: 0, signedDate: "Jan 2000", }], treatyProposals: [{ id: 7, type: TREATY_PEACE, from: 1, to: 0, payload: {}, proposedHours: 0, proposedDate: "Jan 2000", }], treatyHistory: [], localCities: [{ id: 5, name: "Paris" }], localUnits: [], unitProtos: PROTO_UNITS, resources: null, }; const proposals = []; const breaks = []; modal.onProposeTreaty = (type, to, payload) => proposals.push([type, to, payload]); modal.onRespondTreaty = (id, accept) => proposals.push([id, accept]); modal.onCancelTreaty = (id) => breaks.push(id); modal.show(GOVERNMENTS, 0, TECHNOLOGIES, new Set(), 1.0e9, 1.0e15, 1.0e6, {}, info); env.$("#modal-nation .tab[data-tab=diplomacy]").click(); // The pending offer addressed to us carries Sign and Reject. const pending = env.$("#treaty-pending .treaty-row").first(); this.assertTrue(pending.text().includes("Britain proposes Peace")); pending.find("button").first().trigger("click"); this.assertEqual(proposals.at(-1), [7, true], "Sign answers the offer"); // The active alliance offers a Break button. const active = env.$("#treaty-active .treaty-row").first(); this.assertTrue(active.text().includes("Alliance")); active.find("button").trigger("click"); this.assertEqual(breaks, [3], "the alliance can be broken"); // The compose row offers every type and sends the chosen one. env.$("#treaty-type").val(TREATY_GIVE_MONEY).trigger("change"); env.$("#treaty-payload .treaty-money").val("5000"); env.$("#treaty-propose").trigger("click"); this.assertEqual( proposals.at(-1), [TREATY_GIVE_MONEY, 1, { amount: 5000 }], "the money offer carries its amount" ); modal.close(); } finally { teardownDom(env); } } async test_diplomacy_shows_a_foreign_figure_in_its_own_currency_with_ours_beside_it() { const env = await setupDom(); try { setCurrency({ code: "EUR", symbol: "€", name: "Euro" }); setCurrencyBook( [ { civ: 0, symbol: "€", name: "Euro", value: 1 }, { civ: 1, symbol: "₣", name: "Franc", value: 1 }, ], [ { civ: 0, symbol: "€", rate: 1 }, { civ: 1, symbol: "₣", rate: 2 }, ], 0 ); const modal = new NationModal(); const civs = [{ name: "France" }, { name: "Britain" }]; const stats = [ { population: 1.0e6, gdp: 1.0e9, gdpPerCapita: 1000 }, { population: 1.5e6, gdp: 3.0e9, gdpPerCapita: 2000 }, ]; modal.show(GOVERNMENTS, 0, TECHNOLOGIES, new Set(), 1.0e9, 1.0e15, 1.0e6, {}, { civs, localCiv: 0, relations: [], stats, }); env.$("#modal-nation .tab[data-tab=diplomacy]").click(); const britain = env.$("#diplomacy-list tbody tr").eq(0); const gdp = britain.find("td.numeric").eq(0); this.assertTrue(gdp.text().includes("₣6B"), "the foreign GDP reads in its own currency"); this.assertTrue( gdp.find(".conversion").text().includes("€3B"), "our equivalent sits beside it in parentheses" ); const france = env.$("#diplomacy-list tbody tr").eq(1); const own = france.find("td.numeric").eq(0); this.assertTrue(own.text().includes("€1B"), "our own GDP stays in our money"); this.assertSize(own.find(".conversion"), 0, "our own figure needs no conversion"); } finally { setCurrencyBook([], [], -1); setCurrency({ symbol: "¤" }); teardownDom(env); } } async test_diplomacy_table_sorts_by_every_column() { const env = await setupDom(); try { const modal = new NationModal(); const civs = [{ name: "France" }, { name: "Britain" }, { name: "Poland" }]; const stats = [ { population: 1.0e6, gdp: 1.0e9, gdpPerCapita: 1000 }, { population: 3.0e6, gdp: 2.0e9, gdpPerCapita: 2000 }, { population: 2.0e6, gdp: 3.0e9, gdpPerCapita: 3000 }, ]; modal.show( GOVERNMENTS, 0, TECHNOLOGIES, new Set(), 1.0e9, 1.0e15, 1.0e6, {}, { civs, localCiv: 0, relations: [], stats } ); const names = () => env.$("#diplomacy-list tbody tr .name") .map((_, el) => env.$(el).text()).get(); this.assertEqual(names(), ["Britain", "France", "Poland"], "alphabetical by default"); this.assertTrue(env.$("#diplomacy-list th[data-sort=name]").hasClass("sorted-asc")); // GDP ascending first, descending on the second click. env.$("#diplomacy-list th[data-sort=gdp]").click(); this.assertEqual(names(), ["France", "Britain", "Poland"]); this.assertTrue(env.$("#diplomacy-list th[data-sort=gdp]").hasClass("sorted-asc")); env.$("#diplomacy-list th[data-sort=gdp]").click(); this.assertEqual(names(), ["Poland", "Britain", "France"]); this.assertTrue(env.$("#diplomacy-list th[data-sort=gdp]").hasClass("sorted-desc")); // A different column starts ascending. env.$("#diplomacy-list th[data-sort=population]").click(); this.assertEqual(names(), ["France", "Poland", "Britain"]); this.assertTrue(env.$("#diplomacy-list th[data-sort=population]").hasClass("sorted-asc")); // Back to alphabetical. env.$("#diplomacy-list th[data-sort=name]").click(); this.assertEqual(names(), ["Britain", "France", "Poland"]); modal.close(); } finally { teardownDom(env); } } async test_economy_tab_shows_private_cash_central_bank_and_rates() { const env = await setupDom(); try { const modal = new NationModal(); const money = { viewerCiv: 0, currency: { code: "FRF", symbol: "₣", name: "French Franc" }, currencies: [ { civ: 0, code: "FRF", symbol: "₣", name: "French Franc", value: 1 }, { civ: 1, code: "GBP", symbol: "£", name: "British Pound", value: 1 }, ], exchangeRates: [ { civ: 0, code: "FRF", symbol: "₣", name: "French Franc", rate: 1 }, { civ: 1, code: "GBP", symbol: "£", name: "British Pound", rate: 0.85 }, ], centralBank: { rate: 0.02, currency: { code: "FRF", symbol: "₣", name: "French Franc" }, reserves: [{ code: "GBP", symbol: "£", name: "British Pound", amount: 1234 }], loans: 987654, }, privateCash: 5000, // The trade-tax rates, the day's take and the commodity inflation. taxes: { sales: { rate: 0.2, resources: { steel: 0.1 } }, export: { rate: 0.05, resources: {} }, import: { rate: 0.05, resources: {} }, }, takeTax: { sales: 1200, export: 300, import: 100 }, commodityInflation: 0.0123, cities: [{ id: 1, civ: 0, name: "Paris", cashValue: 5000 }], currencyHoldings: { code: "FRF", symbol: "₣", name: "French Franc", value: 1, treasury: 1000, treasuryDebt: 0, own: [{ cityId: 1, name: "Paris", civ: 0, civName: "France", amount: 5000 }], ownTotal: 5000, foreign: [{ cityId: 2, name: "London", civ: 1, civName: "Britain", amount: 700 }], foreignTotal: 700, banks: [{ civ: 1, name: "Britain", amount: 1234 }], bankTotal: 1234, total: 7934, }, }; const resourcesInfo = { summary: { stock: {}, need: {}, production: {}, consumed: { steel: 1200, food: 10, luxury: 0, hightech: 0, energy: 0 }, external: { buy: { steel: { qty: 300, value: 180_000 } }, sell: { food: { qty: 40, value: 12_000 } }, }, prices: {}, }, market: [], }; modal.show( GOVERNMENTS, 0, TECHNOLOGIES, new Set(), 1.0e9, 1.0e15, 1.0e6, { months: [], cities: [] }, null, [], null, null, resourcesInfo, money ); env.$("#modal-nation .tab[data-tab=economy]").click(); this.assertTrue(env.$("#tab-economy").hasClass("active")); const summary = env.$("#economy-summary").text(); this.assertTrue(summary.includes("Paris"), "the region is listed"); this.assertTrue(summary.includes("5 000"), "its private cash is shown"); env.$("#economy-subtabs .subtab[data-view=taxes]").click(); const taxesText = env.$("#economy-taxes").text(); this.assertTrue(taxesText.includes("Sales tax"), "the sales tax is listed"); this.assertTrue(taxesText.includes("Export tariff"), "the export tariff is listed"); this.assertTrue(taxesText.includes("Import tariff"), "the import tariff is listed"); this.assertEqual( env.$("#economy-taxes .tax-input[data-kind=sales]").first().val(), "20.00", "the default sales rate is shown" ); this.assertEqual( env.$("#economy-taxes .tax-input[data-kind=sales][data-resource='steel']").val(), "10.00", "the per-resource override is shown" ); this.assertTrue( env.$("#economy-sub").text().includes("1 600"), "the day's total take is shown" ); env.$("#economy-subtabs .subtab[data-view=centralbank]").click(); const bank = env.$("#economy-central-bank").text(); this.assertTrue(bank.includes("British Pound"), "the foreign reserve is listed"); this.assertTrue(bank.includes("987\u00a0654"), "the outstanding loans are listed"); this.assertTrue(bank.includes("Commodity inflation"), "the inflation indicator is shown"); this.assertTrue(env.$("#cb-rate").length === 1, "the rate input is present"); env.$("#economy-subtabs .subtab[data-view=rates]").click(); this.assertTrue(env.$("#economy-rates").text().includes("British Pound"), "the rate table shows"); this.assertTrue(env.$("#economy-rates").text().includes("0.8500"), "the rate is shown"); env.$("#economy-subtabs .subtab[data-view=consumption]").click(); const consumption = env.$("#economy-consumption").text(); this.assertTrue(consumption.includes("Steel"), "the consumed resource is listed"); this.assertTrue(consumption.includes("1\u00a0200"), "its daily consumption is shown"); this.assertTrue(consumption.includes("180\u00a0000"), "what was spent abroad is shown"); this.assertTrue(consumption.includes("40"), "what was sold abroad is shown"); env.$("#economy-subtabs .subtab[data-view=currency]").click(); const currency = env.$("#economy-currency").text(); this.assertTrue( env.$("#economy-sub").text().includes("French Franc"), "the currency is named" ); this.assertTrue(currency.includes("Paris"), "a home region holding it is listed"); this.assertTrue(currency.includes("London"), "a foreign region holding it is listed"); this.assertTrue(currency.includes("Central bank of Britain"), "a foreign bank is listed"); this.assertTrue(currency.includes("7 934"), "the total in circulation is shown"); } finally { teardownDom(env); } } async test_central_bank_charts_commodity_prices() { const env = await setupDom(); try { const modal = new NationModal(); const money = { viewerCiv: 0, currency: { code: "FRF", symbol: "₣", name: "French Franc" }, currencies: [{ civ: 0, code: "FRF", symbol: "₣", name: "French Franc", value: 1 }], exchangeRates: [{ civ: 0, code: "FRF", symbol: "₣", name: "French Franc", rate: 1 }], centralBank: { rate: 0.02, currency: { code: "FRF", symbol: "₣", name: "French Franc" }, reserves: [], loans: 0, }, taxes: { sales: { rate: 0.2, resources: {} }, export: { rate: 0.05, resources: {} }, import: { rate: 0.05, resources: {} }, }, takeTax: { sales: 0, export: 0, import: 0 }, commodityInflation: 0, currencyHoldings: null, privateCash: 0, cities: [], resourcePriceHistory: [ ["energy", [100, 110, 120]], ["food", [100, 95, 105]], ], resourceInflation: [ { id: "energy", index: 120, monthly: 0.09, yearly: 0.2 }, { id: "food", index: 105, monthly: 0.105, yearly: 0.05 }, ], }; const resourcesInfo = { summary: { stock: {}, need: {}, production: {}, prices: {}, consumed: { steel: 0, food: 0, luxury: 0, hightech: 0, energy: 0 }, external: { buy: {}, sell: {} }, }, market: [], trade: [], }; modal.show( GOVERNMENTS, 0, TECHNOLOGIES, new Set(), 1.0e9, 1.0e15, 1.0e6, { months: [], cities: [] }, null, [], null, null, resourcesInfo, money ); env.$("#modal-nation .tab[data-tab=economy]").click(); env.$("#economy-subtabs .subtab[data-view=centralbank]").click(); this.assertSize(env.$("#economy-central-bank .line-chart"), 1, "the index chart is drawn"); this.assertSize( env.$("#economy-central-bank .line-chart-line"), 2, "one line per commodity" ); const table = env.$("#economy-central-bank .inflation-table").text(); this.assertTrue(table.includes("Energy"), "energy is tabled"); this.assertTrue(table.includes("Food"), "food is tabled"); this.assertTrue(table.includes("+9.00%"), "monthly inflation is shown"); this.assertTrue(table.includes("+20.00%"), "yearly inflation is shown"); this.assertTrue( env.$("#economy-sub").text().includes("Commodity inflation"), "the headline is shown" ); } finally { teardownDom(env); } } async test_central_bank_chart_rolls_a_ninety_day_window() { const env = await setupDom(); try { const modal = new NationModal(); // One hundred and twenty daily points, so thirty fall off the ninety-day // window. The clock is day 120, so the window's first day is day 31. const energy = Array.from({ length: 120 }, (_, i) => 100 + i); const money = { viewerCiv: 0, currency: { code: "FRF", symbol: "₣", name: "French Franc" }, currencies: [{ civ: 0, code: "FRF", symbol: "₣", name: "French Franc", value: 1 }], exchangeRates: [{ civ: 0, code: "FRF", symbol: "₣", name: "French Franc", rate: 1 }], centralBank: { rate: 0.02, reserves: [], loans: 0 }, taxes: { sales: { rate: 0.2, resources: {} }, export: { rate: 0.05, resources: {} }, import: { rate: 0.05, resources: {} }, }, takeTax: { sales: 0, export: 0, import: 0 }, commodityInflation: 0, currencyHoldings: null, privateCash: 0, cities: [], totalHours: 120 * 24, resourcePriceHistory: [["energy", energy]], resourceInflation: [{ id: "energy", index: 219, monthly: 0.03, yearly: 0.25 }], }; const resourcesInfo = { summary: { stock: {}, need: {}, production: {}, prices: {}, consumed: { steel: 0, food: 0, luxury: 0, hightech: 0, energy: 0 }, external: { buy: {}, sell: {} }, }, market: [], trade: [], }; modal.show( GOVERNMENTS, 0, TECHNOLOGIES, new Set(), 1.0e9, 1.0e15, 1.0e6, { months: [], cities: [] }, null, [], null, null, resourcesInfo, money ); env.$("#modal-nation .tab[data-tab=economy]").click(); env.$("#economy-subtabs .subtab[data-view=centralbank]").click(); const points = env.$("#economy-central-bank .line-chart-line").attr("points") || ""; this.assertEqual(points.trim().split(/\s+/).length, 90, "the window holds 90 days"); const months = env.$("#economy-central-bank .line-chart-month"); this.assertEqual(months.length, 3, "one label per month in the window"); this.assertEqual(months.eq(0).text(), "Feb", "the window starts in February"); this.assertEqual(months.eq(1).text(), "Mar"); this.assertEqual(months.eq(2).text(), "Apr", "and ends in April"); } finally { teardownDom(env); } } async test_central_bank_chart_leaves_future_days_blank() { const env = await setupDom(); try { const modal = new NationModal(); // Ten daily points: the line must cover ten of the ninety slots, with the // rest of the fixed axis left for days the game has not reached. const energy = Array.from({ length: 10 }, (_, i) => 100 + i); const money = { viewerCiv: 0, currency: { code: "FRF", symbol: "₣", name: "French Franc" }, currencies: [{ civ: 0, code: "FRF", symbol: "₣", name: "French Franc", value: 1 }], exchangeRates: [{ civ: 0, code: "FRF", symbol: "₣", name: "French Franc", rate: 1 }], centralBank: { rate: 0.02, reserves: [], loans: 0 }, taxes: { sales: { rate: 0.2, resources: {} }, export: { rate: 0.05, resources: {} }, import: { rate: 0.05, resources: {} }, }, takeTax: { sales: 0, export: 0, import: 0 }, commodityInflation: 0, currencyHoldings: null, privateCash: 0, cities: [], totalHours: 9 * 24, resourcePriceHistory: [["energy", energy]], resourceInflation: [{ id: "energy", index: 109, monthly: 0.01, yearly: 0.09 }], }; const resourcesInfo = { summary: { stock: {}, need: {}, production: {}, prices: {}, consumed: { steel: 0, food: 0, luxury: 0, hightech: 0, energy: 0 }, external: { buy: {}, sell: {} }, }, market: [], trade: [], }; modal.show( GOVERNMENTS, 0, TECHNOLOGIES, new Set(), 1.0e9, 1.0e15, 1.0e6, { months: [], cities: [] }, null, [], null, null, resourcesInfo, money ); env.$("#modal-nation .tab[data-tab=economy]").click(); env.$("#economy-subtabs .subtab[data-view=centralbank]").click(); const chart = env.$("#economy-central-bank .line-chart").get(0); const viewBox = (chart.getAttribute("viewBox") || "").split(/\s+/); const points = env.$("#economy-central-bank .line-chart-line").attr("points") || ""; const last = points.trim().split(/\s+/).pop().split(","); const plotW = Number(viewBox[2]) - 40 - 10; this.assertApprox( Number(last[0]), 40 + (9 / 89) * plotW, 0.5, "ten points sit on the first ten of the ninety fixed slots, not stretched" ); } finally { teardownDom(env); } } } export class ResourceTrendTest extends TestCase { async test_resources_tab_marks_day_on_day_changes() { const env = await setupDom(); try { const modal = new NationModal(); const info = (price, totalHours) => ({ totalHours, summary: { stock: { food: 100, steel: 100, luxury: 100, hightech: 0 }, consumed: { food: 10, steel: 10, luxury: 10, hightech: 0, energy: 100 }, production: { food: 10, steel: 10, luxury: 10, hightech: 0 }, energy: { need: 100, supply: 100 }, trade: { imports: {}, exports: {} }, }, market: [{ id: "food", price, prevPrice: price, supply: 1000, demand: 1000 }], trade: [], civs: [{ name: "France" }], }); modal._renderResources(info(100, 0)); this.assertEqual( env.$("#resources-list .stat-arrow").length, 0, "the first day has no baseline to compare against" ); modal._renderResources(info(120, 24)); this.assertTrue( env.$("#resources-list .stat-arrow").length > 0, "the second day marks the changes" ); } finally { teardownDom(env); } } } export class ConfirmModalTest extends TestCase { async test_confirm_runs_the_callback_and_cancel_does_not() { const env = await setupDom(); try { const modal = new ConfirmModal(); let confirmed = 0; modal.show("Declare war?", "Marching in will declare war.", () => { confirmed += 1; }); this.assertTrue(modal.isShowing()); this.assertEqual(env.$("#confirm-title").text(), "Declare war?"); this.assertTrue(env.$("#confirm-message").text().includes("declare war")); env.$("#confirm-cancel").click(); this.assertFalse(modal.isShowing()); this.assertEqual(confirmed, 0, "cancelling runs nothing"); modal.show("Again", "Sure?", () => { confirmed += 1; }); env.$("#confirm-ok").click(); this.assertEqual(confirmed, 1, "confirming runs the callback once"); this.assertFalse(modal.isShowing()); } finally { teardownDom(env); } } async test_close_button_cancels() { const env = await setupDom(); try { const modal = new ConfirmModal(); let confirmed = 0; modal.show("T", "M", () => { confirmed += 1; }); env.$("#modal-confirm .modal-close").click(); this.assertEqual(confirmed, 0); this.assertFalse(modal.isShowing()); } finally { teardownDom(env); } } } export class NewsModalTest extends TestCase { async test_lists_events_newest_first_with_dates() { const env = await setupDom(); try { const modal = new NewsModal(); const civs = [{ name: "France", demonym: "French" }, { name: "Britain", demonym: "British" }]; const news = [ { id: 1, type: "war", actor: 0, target: 1, date: "1 January 2000, 00:00" }, { id: 2, type: "capture", city: "Lyon", from: 1, to: 0, date: "2 January 2000, 05:00" }, ]; modal.show(news, civs); const items = env.$("#news-list .news-item"); this.assertSize(items, 2); this.assertTrue( items.eq(0).find(".news-text").text().includes("French troops have captured Lyon from the British") ); this.assertTrue(items.eq(0).find(".news-date").text().includes("05:00")); this.assertTrue( items.eq(1).find(".news-text").text().includes("French leaders have declared war on Britain") ); modal.refresh([], civs); this.assertEqual(env.$("#news-list .news-item").length, 0); this.assertTrue(env.$("#news-list").text().includes("No news")); modal.close(); this.assertTrue(env.$("#modal-news").hasClass("hidden")); } finally { teardownDom(env); } } test_news_names_the_actors_doing_the_action() { const civs = [ { name: "France", demonym: "French" }, { name: "Vietnam", demonym: "Vietnamese" }, { name: "Aotearoa", demonym: "New Zealander", adjective: "New Zealand" }, ]; this.assertEqual( describeNews({ type: "capture", city: "Saigon", from: 1, to: 0 }, civs), "French troops have captured Saigon from the Vietnamese" ); this.assertEqual( describeNews({ type: "technology", civ: 0, name: "Radio" }, civs), "French scientists have discovered Radio" ); this.assertEqual( describeNews({ type: "technology", civ: 0, name: "Railways", repeatable: true }, civs), "French scientists have improved Railways" ); this.assertEqual( describeNews({ type: "war", actor: 0, target: 1 }, civs), "French leaders have declared war on Vietnam" ); this.assertEqual( describeNews({ type: "policy", civ: 0, policy: "some_policy", ethnicity: 1 }, civs), "French lawmakers have enacted some_policy for the Vietnamese" ); this.assertEqual( describeNews({ type: "join", player: "Alice", civ: 1 }, civs), "Alice took control of Vietnam" ); // Person-noun demonyms carry an explicit attributive form. this.assertEqual( describeNews({ type: "technology", civ: 2, name: "Radio" }, civs), "New Zealand scientists have discovered Radio" ); this.assertEqual( describeNews({ type: "famine", civ: 1, region: "Hanoi" }, civs), "the Vietnamese are starving in Hanoi: a famine has begun" ); } }