Skip to content

Commit

Permalink
NNS1-3486: period filter for reporting transactions (#6032)
Browse files Browse the repository at this point in the history
# Motivation

We want users to be able to generate three types of reports:  
* full history  
* year-to-date  
* last year  


https://qsgjb-riaaa-aaaaa-aaaga-cai.yhabib-ingress.devenv.dfinity.network/reporting/

# Changes

* Adds `period` state to `ReportingTransactions`. It binds it to
`ReportingDateRangeSelector` so it can be updated, and it passes it down
to `ReportingTransactionsButton` to be used when generating the report.

# Tests

* Unit tests for `ReportingTransactions`

# Todos

- [x] Add entry to changelog (if necessary).
  • Loading branch information
yhabib authored Dec 19, 2024
1 parent 50f8c21 commit 832d650
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 12 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG-Nns-Dapp-unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ proposal is successful, the changes it released will be moved from this file to

#### Added

- Reporting: Full period filter, year-to-date, and last year

#### Changed

#### Deprecated
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { i18n } from "$lib/stores/i18n";
import type { ReportingPeriod } from "$lib/types/reporting";
let period: ReportingPeriod = "all";
export let period: ReportingPeriod = "all";
const options: Array<{
value: ReportingPeriod;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
<script lang="ts">
import { i18n } from "$lib/stores/i18n";
import type { ReportingPeriod } from "$lib/types/reporting";
import ReportingDateRangeSelector from "./ReportingDateRangeSelector.svelte";
import ReportingTransactionsButton from "./ReportingTransactionsButton.svelte";
// TODO: This will be hooked up in a follow up
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let selectedRange: ReportingPeriod = "all";
let period: ReportingPeriod = "all";
</script>

<div class="wrapper">
<div class="wrapper" data-tid="reporting-transactions-component">
<div>
<h3>{$i18n.reporting.transactions_title}</h3>
<p class="description">{$i18n.reporting.transactions_description}</p>
</div>
<ReportingTransactionsButton />

<ReportingDateRangeSelector bind:period />
<ReportingTransactionsButton {period} />
</div>

<style lang="scss">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,55 @@
import ReportingDateRangeSelector from "$lib/components/reporting/ReportingDateRangeSelector.svelte";
import type { ReportingPeriod } from "$lib/types/reporting";
import { JestPageObjectElement } from "$tests/page-objects/jest.page-object";
import { ReportingDateRangeSelectorPo } from "$tests/page-objects/ReportingDateRangeSelector.page-object";
import { render } from "@testing-library/svelte";
import { tick } from "svelte";

describe("ReportingDateRangeSelector", () => {
const renderComponent = () => {
const { container } = render(ReportingDateRangeSelector);
const renderComponent = (
{
period,
}: {
period?: ReportingPeriod;
} = { period: "all" }
) => {
const { container, component } = render(ReportingDateRangeSelector, {
period,
});

const po = ReportingDateRangeSelectorPo.under({
element: new JestPageObjectElement(container),
});

return po;
return { po, component };
};

const getComponentPropValue = (component, propName: string) => {
return component.$$.ctx[component.$$.props[propName]];
};

it("should render the option provided as a prop", async () => {
const { po } = renderComponent({ period: "last-year" });

const selectedOption = po.getSelectedOption();
expect(await selectedOption.getValue()).toBe("last-year");
});

it("should render three options", async () => {
const po = renderComponent();
const { po } = renderComponent();

expect(await po.getAllOptions()).toHaveLength(3);
});

it("should select 'all' option by default", async () => {
const po = renderComponent();
const { po } = renderComponent();

const selectedOption = po.getSelectedOption();
expect(await selectedOption.getValue()).toBe("all");
});

it("should change the option when interacting with a new element", async () => {
const po = renderComponent();
const { po } = renderComponent();
const allOptions = await po.getAllOptions();
const firstOptionValue = await allOptions[0].getValue();
const secondOption = allOptions[1];
Expand All @@ -41,4 +63,18 @@ describe("ReportingDateRangeSelector", () => {

expect(await currentOption.getValue()).toBe(await secondOption.getValue());
});

it("should update exported prop when selecting an option", async () => {
const { po, component } = renderComponent();
const allOptions = await po.getAllOptions();

let currentValue = getComponentPropValue(component, "period");
expect(currentValue).toBe("all");

await allOptions[1].click();
await tick();

currentValue = getComponentPropValue(component, "period");
expect(currentValue).toBe("last-year");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import * as governanceApi from "$lib/api/governance.api";
import * as icpIndexApi from "$lib/api/icp-index.api";
import ReportingTransactions from "$lib/components/reporting/ReportingTransactions.svelte";
import * as exportDataService from "$lib/services/reporting.services";
import * as exportToCsv from "$lib/utils/reporting.utils";
import { mockIdentity, resetIdentity } from "$tests/mocks/auth.store.mock";
import {
mockAccountsStoreData,
mockMainAccount,
} from "$tests/mocks/icp-accounts.store.mock";
import { ReportingTransactionsPo } from "$tests/page-objects/ReportingTransactions.page-object";
import { JestPageObjectElement } from "$tests/page-objects/jest.page-object";
import {
resetAccountsForTesting,
setAccountsForTesting,
} from "$tests/utils/accounts.test-utils";
import { render } from "@testing-library/svelte";

vi.mock("$lib/api/icp-ledger.api");
vi.mock("$lib/api/governance.api");

describe("ReportingTransactions", () => {
let getAccountTransactionsConcurrently;

const renderComponent = () => {
const { container } = render(ReportingTransactions);

const po = ReportingTransactionsPo.under({
element: new JestPageObjectElement(container),
});
return po;
};

beforeEach(() => {
vi.clearAllTimers();
resetIdentity();
resetAccountsForTesting();

vi.spyOn(exportToCsv, "generateCsvFileToSave").mockImplementation(() =>
Promise.resolve()
);
vi.spyOn(governanceApi, "queryNeurons").mockResolvedValue([]);
vi.spyOn(console, "error").mockImplementation(() => {});
vi.spyOn(icpIndexApi, "getTransactions").mockResolvedValue({
transactions: [],
balance: 0n,
oldestTxId: 1n,
});

const mockDate = new Date("2023-10-14T00:00:00Z");
vi.useFakeTimers();
vi.setSystemTime(mockDate);

setAccountsForTesting({
...mockAccountsStoreData,
});

getAccountTransactionsConcurrently = vi.spyOn(
exportDataService,
"getAccountTransactionsConcurrently"
);
});

it("should fetch all transactions by default", async () => {
const po = renderComponent();
await po.getReportingTransactionsButtonPo().click();

expect(getAccountTransactionsConcurrently).toHaveBeenCalledTimes(1);
expect(getAccountTransactionsConcurrently).toHaveBeenCalledWith({
entities: [mockMainAccount],
identity: mockIdentity,
range: {},
});
});

it("should fetch year-to-date transactions when selecting such option", async () => {
const beginningOfYear = new Date("2023-01-01T00:00:00Z");
const NANOS_IN_MS = BigInt(1_000_000);
const beginningOfYearInNanoseconds =
BigInt(beginningOfYear.getTime()) * NANOS_IN_MS;

const po = renderComponent();
await po
.getReportingDateRangeSelectorPo()
.selectProvidedOption("year-to-date");
await po.getReportingTransactionsButtonPo().click();

expect(getAccountTransactionsConcurrently).toHaveBeenCalledTimes(1);
expect(getAccountTransactionsConcurrently).toHaveBeenCalledWith({
entities: [mockMainAccount],
identity: mockIdentity,
range: {
from: beginningOfYearInNanoseconds,
},
});
});

it("should fetch last-year transactions when selecting such option", async () => {
const beginningOfYear = new Date("2023-01-01T00:00:00Z");
const beginningOfLastYear = new Date("2022-01-01T00:00:00Z");
const NANOS_IN_MS = BigInt(1_000_000);
const beginningOfYearInNanoseconds =
BigInt(beginningOfYear.getTime()) * NANOS_IN_MS;

const beginningOfLastYearInNanoseconds =
BigInt(beginningOfLastYear.getTime()) * NANOS_IN_MS;

const po = renderComponent();
await po
.getReportingDateRangeSelectorPo()
.selectProvidedOption("last-year");
await po.getReportingTransactionsButtonPo().click();

expect(getAccountTransactionsConcurrently).toHaveBeenCalledTimes(1);
expect(getAccountTransactionsConcurrently).toHaveBeenCalledWith({
entities: [mockMainAccount],
identity: mockIdentity,
range: {
from: beginningOfLastYearInNanoseconds,
to: beginningOfYearInNanoseconds,
},
});
});
});
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ReportingPeriod } from "$lib/types/reporting";
import type { PageObjectElement } from "$tests/types/page-object.types";
import { SimpleBasePageObject } from "./simple-base.page-object";

Expand All @@ -21,4 +22,18 @@ export class ReportingDateRangeSelectorPo extends SimpleBasePageObject {
getSelectedOption() {
return this.getElement().querySelector('input[type="radio"]:checked');
}

async selectProvidedOption(option: ReportingPeriod) {
const allOptions = await this.getAllOptions();

for (const opt of allOptions) {
const value = await opt.getValue();
if (value === option) {
await opt.click();
return;
}
}

throw new Error(`Option ${option} not found`);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { PageObjectElement } from "$tests/types/page-object.types";
import { ReportingDateRangeSelectorPo } from "./ReportingDateRangeSelector.page-object";
import { ReportingTransactionsButtonPo } from "./ReportingTransactionsButton.page-object";
import { BasePageObject } from "./base.page-object";

export class ReportingTransactionsPo extends BasePageObject {
static readonly TID = "reporting-transactions-component";

static under({
element,
}: {
element: PageObjectElement;
}): ReportingTransactionsPo {
return new ReportingTransactionsPo(
element.byTestId(ReportingTransactionsPo.TID)
);
}

getReportingDateRangeSelectorPo() {
return ReportingDateRangeSelectorPo.under({
element: this.root,
});
}

getReportingTransactionsButtonPo() {
return ReportingTransactionsButtonPo.under({
element: this.root,
});
}
}

0 comments on commit 832d650

Please sign in to comment.