import { click, visit } from "@ember/test-helpers"; import { test } from "qunit"; import { cloneJSON } from "discourse/lib/object"; import topicFixtures from "discourse/tests/fixtures/topic"; import { acceptance } from "discourse/tests/helpers/qunit-helpers"; const TOPIC_ID = 28830; const accountsReport = { report_type: "accounts", period: { begin: null, end: null }, sections: [ { title: null, rows: [ { account: "assets", label: "Assets", depth: 0, root: true, amounts: [ { commodity: "EUR", quantity: "1000.0", display: "1000.00 EUR" }, ], }, { account: "assets:bank", label: "Bank", depth: 1, root: false, amounts: [ { commodity: "EUR", quantity: "1000.0", display: "1000.00 EUR" }, ], }, { account: "assets:bank:checking", label: "Checking", depth: 2, root: false, amounts: [ { commodity: "EUR", quantity: "1000.0", display: "1000.00 EUR" }, ], }, { account: "equity", label: "Equity", depth: 0, root: true, amounts: [ { commodity: "EUR", quantity: "-1000.0", display: "(1000.00 EUR)" }, ], }, { account: "equity:members:@tony", label: "@tony", depth: 2, root: false, mention: { username: "tony", text: "@tony" }, amounts: [ { commodity: "EUR", quantity: "-1000.0", display: "(1000.00 EUR)" }, ], }, { account: "equity:members:@nobody", label: "@nobody", depth: 2, root: false, amounts: [{ commodity: "EUR", quantity: "0.0", display: "0.00 EUR" }], }, ], total: [{ commodity: "EUR", quantity: "1000.0", display: "1000.00 EUR" }], }, ], net: null, meta: { hledger_version: "1.52.1" }, }; const equityReport = { report_type: "equity", period: { begin: null, end: null }, groups: [ { commodity: "EUR", total: { commodity: "EUR", quantity: "1000.0", display: "1000.0 EUR" }, rows: [ { account: "equity:alice", label: "Alice", depth: 0, amounts: [ { commodity: "EUR", quantity: "600.0", display: "600.0 EUR" }, ], percentage: "60.00", }, { account: "equity:bob", label: "Bob", depth: 0, amounts: [ { commodity: "EUR", quantity: "400.0", display: "400.0 EUR" }, ], percentage: "40.00", }, ], }, ], meta: { hledger_version: "1.52.1" }, }; const balanceSheetReport = { report_type: "balance_sheet", period: { begin: null, end: null }, sections: [ { title: "Assets", rows: [ { account: "assets:bank", label: "Bank", depth: 0, root: false, amounts: [ { commodity: "EUR", quantity: "1000.0", display: "1000.00 EUR" }, ], }, { account: "assets:bank:checking", label: "Checking", depth: 1, root: false, amounts: [ { commodity: "EUR", quantity: "1000.0", display: "1000.00 EUR" }, ], }, ], total: [{ commodity: "EUR", quantity: "1000.0", display: "1000.00 EUR" }], }, { title: "Liabilities", rows: [], total: [{ commodity: "EUR", quantity: "0.0", display: "0.00 EUR" }], }, ], net: [{ commodity: "EUR", quantity: "1000.0", display: "1000.00 EUR" }], meta: { hledger_version: "1.52.1" }, }; const incomeStatementReport = { report_type: "income_statement", period: { begin: null, end: null }, sections: [ { title: "Revenues", rows: [ { account: "revenues:donations", label: "Donations", depth: 0, root: false, amounts: [ { commodity: "EUR", quantity: "50.0", display: "50.00 EUR" }, ], }, ], total: [{ commodity: "EUR", quantity: "50.0", display: "50.00 EUR" }], }, { title: "Expenses", rows: [ { account: "expenses:hosting", label: "Hosting", depth: 0, root: false, amounts: [ { commodity: "EUR", quantity: "20.5", display: "20.50 EUR" }, ], }, { account: "expenses:hosting:server", label: "Server", depth: 1, root: false, amounts: [ { commodity: "EUR", quantity: "20.5", display: "20.50 EUR" }, ], }, ], total: [{ commodity: "EUR", quantity: "20.5", display: "20.50 EUR" }], }, ], net: [{ commodity: "EUR", quantity: "29.5", display: "29.50 EUR" }], meta: { hledger_version: "1.52.1" }, }; const transactionsReport = { report_type: "transactions", period: { begin: null, end: null }, transactions: [ { date: "2024-01-01", date2: null, description: "Opening balances", code: "", postings: [ { account: "assets:bank:checking", amounts: [ { commodity: "EUR", quantity: "1000.0", display: "1000.00 EUR" }, ], }, { account: "equity:alice", amounts: [ { commodity: "EUR", quantity: "-600.0", display: "-600.00 EUR" }, ], }, { account: "equity:bob", amounts: [ { commodity: "EUR", quantity: "-400.0", display: "-400.00 EUR" }, ], }, { account: "equity:members:@tony", account_segments: [ { text: "equity" }, { text: "members" }, { text: "@tony", username: "tony" }, ], amounts: [ { commodity: "EUR", quantity: "-600.0", display: "-600.00 EUR" }, ], }, ], }, { date: "2024-02-10", date2: null, description: "Hosting", code: "", postings: [ { account: "expenses:hosting:server", amounts: [ { commodity: "EUR", quantity: "20.5", display: "20.50 EUR" }, ], }, { account: "assets:bank:checking", amounts: [ { commodity: "EUR", quantity: "-20.5", display: "-20.50 EUR" }, ], }, ], }, ], meta: { hledger_version: "1.52.1" }, }; acceptance("Hledger plugin", function (needs) { needs.settings({ hledger_enabled: true }); needs.pretender((server, helper) => { server.get("/t/45.json", () => { const topic = cloneJSON(topicFixtures["/t/28830/1.json"]); topic.post_stream.posts[0].cooked = `
`; return helper.response(topic); }); server.get(`/hledger/topics/${TOPIC_ID}/reports/accounts`, () => helper.response(accountsReport) ); server.get(`/hledger/topics/${TOPIC_ID}/reports/equity`, () => helper.response(equityReport) ); server.get(`/hledger/topics/${TOPIC_ID}/reports/balance_sheet`, () => helper.response(balanceSheetReport) ); server.get(`/hledger/topics/${TOPIC_ID}/reports/income_statement`, () => helper.response(incomeStatementReport) ); server.get(`/hledger/topics/${TOPIC_ID}/reports/transactions`, () => helper.response(transactionsReport) ); }); test("renders the dashboard in place of the journal", async function (assert) { await visit("/t/-/45"); assert.dom(".hledger-dashboard__toolbar .btn").exists({ count: 5 }); assert .dom(".hledger-dashboard__toolbar .btn:nth-of-type(1)") .hasText("Accounts"); assert .dom(".hledger-dashboard__toolbar .btn:nth-of-type(2)") .hasText("Balance sheet"); assert .dom(".hledger-dashboard__toolbar .btn:nth-of-type(3)") .hasText("P&L"); assert .dom(".hledger-dashboard__toolbar .btn:nth-of-type(4)") .hasText("Equity"); assert .dom(".hledger-dashboard__toolbar .btn:nth-of-type(5)") .hasText("Journal"); assert .dom( ".hledger-dashboard__toolbar .btn:nth-of-type(2) .d-icon-scale-balanced" ) .exists("renders the balance sheet icon"); assert .dom(".hledger-dashboard__toolbar .btn:nth-of-type(3) .d-icon-chart-line") .exists("renders the income statement icon"); assert.dom(".hledger-dashboard__table").includesText("Total"); assert.dom(".hledger-dashboard__table").includesText("Assets"); assert.dom(".hledger-dashboard__table").includesText("Bank"); assert.dom(".hledger-dashboard__table").includesText("Checking"); assert.dom(".hledger-dashboard__table").includesText("1000.00 EUR"); assert .dom(".hledger-dashboard__table") .includesText("(1000.00 EUR)", "shows credit balances in parentheses"); assert.dom(".hledger-dashboard__account.--depth-1").hasText("Bank"); assert.dom(".hledger-dashboard__account.--depth-2").hasText("Checking"); assert .dom('.hledger-dashboard__account[title="assets:bank"]') .hasText("Bank"); assert .dom('a.mention[href="/u/tony"][data-user-card="tony"]') .hasText("@tony", "links a mentioned account to its user"); assert .dom('.hledger-dashboard__account[title="equity:members:@nobody"] a') .doesNotExist("leaves an unknown mention as plain text"); assert .dom('.hledger-dashboard__account[title="equity:members:@nobody"]') .hasText("@nobody"); const rootAccount = document.querySelector( ".hledger-dashboard__account.--depth-0" ); const childAccount = document.querySelector( ".hledger-dashboard__account.--depth-1" ); const grandchildAccount = document.querySelector( ".hledger-dashboard__account.--depth-2" ); const amountCell = rootAccount .closest("tr") .querySelector(".hledger-dashboard__amount"); const paddingStart = (element) => parseFloat( getComputedStyle(element).getPropertyValue("padding-inline-start") ); assert.true( paddingStart(childAccount) > paddingStart(rootAccount), "indents child accounts" ); assert.true( paddingStart(grandchildAccount) > paddingStart(childAccount), "indents grandchild accounts" ); assert.true( ["end", "right"].includes(getComputedStyle(amountCell).textAlign), "aligns amounts to the end" ); assert.true( parseInt( getComputedStyle( document.querySelector(".hledger-dashboard__account.--root") ).fontWeight, 10 ) >= 700, "bolds root accounts" ); }); test("switches to the equity distribution", async function (assert) { await visit("/t/-/45"); await click(".hledger-dashboard__toolbar .btn:nth-of-type(4)"); assert.dom(".hledger-dashboard__table").includesText("Alice"); assert.dom(".hledger-dashboard__table").includesText("60.00%"); assert .dom('.hledger-dashboard__account[title="equity:alice"]') .hasText("Alice"); assert.dom(".hledger-dashboard__account.--depth-0").exists({ count: 2 }); assert .dom(".hledger-dashboard__account.--root") .doesNotExist("equity rows are never bold"); }); test("renders the balance sheet and income statement as trees", async function (assert) { await visit("/t/-/45"); await click(".hledger-dashboard__toolbar .btn:nth-of-type(2)"); assert.dom(".hledger-dashboard__section-title").exists({ count: 2 }); assert .dom('.hledger-dashboard__account[title="assets:bank"]') .hasText("Bank"); assert .dom('.hledger-dashboard__account[title="assets:bank:checking"]') .hasText("Checking"); assert .dom(".hledger-dashboard__account.--depth-0") .hasText("Bank", "starts the tree flush under the heading"); assert.dom(".hledger-dashboard__account.--depth-1").hasText("Checking"); assert .dom(".hledger-dashboard__account.--root") .doesNotExist("no root rows once the heading stands in for the root"); const bank = document.querySelector( '.hledger-dashboard__account[title="assets:bank"]' ); const checking = document.querySelector( '.hledger-dashboard__account[title="assets:bank:checking"]' ); assert.true( parseFloat( getComputedStyle(checking).getPropertyValue("padding-inline-start") ) > parseFloat( getComputedStyle(bank).getPropertyValue("padding-inline-start") ), "indents below the heading's first level" ); assert .dom(".hledger-dashboard__empty") .exists({ count: 1 }, "shows a placeholder for the empty section"); assert.dom(".hledger-dashboard__empty").hasText("No data"); assert .dom( ".hledger-dashboard__section:nth-of-type(2) .hledger-dashboard__table" ) .doesNotExist("omits the table for the empty section"); assert .dom( ".hledger-dashboard__section:nth-of-type(1) .hledger-dashboard__table" ) .exists("keeps the table for the populated section"); await click(".hledger-dashboard__toolbar .btn:nth-of-type(3)"); assert .dom('.hledger-dashboard__account[title="revenues:donations"]') .hasText("Donations"); assert .dom('.hledger-dashboard__account[title="expenses:hosting:server"]') .hasText("Server"); }); test("shows the transaction log", async function (assert) { await visit("/t/-/45"); await click(".hledger-dashboard__toolbar .btn:nth-of-type(5)"); assert .dom(".hledger-dashboard__transaction") .exists({ count: 2 }, "renders one block per transaction"); assert .dom( ".hledger-dashboard__transaction:nth-of-type(1) .hledger-dashboard__transaction-date" ) .hasText("2024-01-01", "shows the transaction date"); assert .dom( ".hledger-dashboard__transaction:nth-of-type(1) .hledger-dashboard__transaction-description" ) .hasText("Opening balances", "shows the transaction description"); assert .dom(".hledger-dashboard__table") .includesText("assets:bank:checking"); assert.dom(".hledger-dashboard__table").includesText("1000.00 EUR"); assert .dom(".hledger-dashboard__table") .includesText("equity:members:@tony", "keeps path separators"); assert .dom('a.mention[href="/u/tony"][data-user-card="tony"]') .hasText("@tony", "links a mentioned account in the journal"); }); });