Skip to content

Commit ed9fc09

Browse files
committed
feat(statistics): implement statistics page
1 parent a4e234e commit ed9fc09

15 files changed

Lines changed: 2144 additions & 5 deletions

File tree

__tests__/e2e/statistics.test.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { expect, test } from "@playwright/test";
2+
3+
test.beforeEach(async ({ page }) => {
4+
await page.goto("http://localhost:3000/statistics/");
5+
});
6+
7+
test.describe("Statistics", () => {
8+
test("should display cover title", async ({ page }) => {
9+
await expect(page.locator("h1")).toHaveText("Statistics");
10+
});
11+
12+
test("should display summary cards", async ({ page }) => {
13+
const cards = page
14+
.locator("main section > div")
15+
.first()
16+
.locator("> a, > div");
17+
await expect(cards).toHaveCount(4);
18+
19+
// NOTE: the mock backend's fixtures return 10 posts, 5 tags and 3 series.
20+
const postsCard = cards.nth(0);
21+
await expect(postsCard).toContainText("10");
22+
await expect(postsCard).toContainText("posts");
23+
24+
await expect(cards.nth(1)).toContainText("5");
25+
await expect(cards.nth(1)).toContainText("tags");
26+
await expect(cards.nth(2)).toContainText("3");
27+
await expect(cards.nth(2)).toContainText("series");
28+
// NOTE: "years" depends on the current date, so assert the label only.
29+
await expect(cards.nth(3)).toContainText("years");
30+
31+
await expect(cards.nth(0)).toHaveAttribute("href", "/archives");
32+
await expect(cards.nth(1)).toHaveAttribute("href", "/tags");
33+
await expect(cards.nth(2)).toHaveAttribute("href", "/series");
34+
35+
for (let i = 0; i < 4; i++) {
36+
const value = await cards.nth(i).locator("> div").first().textContent();
37+
expect(value).toMatch(/^\d+$/);
38+
}
39+
});
40+
41+
test("should display activity charts and switch year and timezone by dropdowns - with screenshot", async ({
42+
page
43+
}, testInfo) => {
44+
const dropdowns = page.locator("main section select");
45+
await expect(dropdowns).toHaveCount(2);
46+
const yearDropdown = dropdowns.nth(0);
47+
const timeZoneDropdown = dropdowns.nth(1);
48+
49+
// recharts renders svg bar charts (monthly / day of week / hour)
50+
await expect(page.getByText("Monthly Posts")).toBeVisible();
51+
await expect(page.getByText("Posts by Day of Week")).toBeVisible();
52+
await expect(page.getByText("Posts by Hour (Asia/Tokyo)")).toBeVisible();
53+
// auto-retrying assertion: at least 3 svg charts are rendered
54+
await expect(page.locator("main section svg").nth(2)).toBeVisible();
55+
56+
// punch card renders a 7x24 grid
57+
await expect(page.getByText("Punch Card (Asia/Tokyo)")).toBeVisible();
58+
await expect(page.locator("main section [data-count]")).toHaveCount(168);
59+
60+
// hovering a punch card cell shows a tooltip
61+
const filledCell = page
62+
.locator('main section [data-count]:not([data-count="0"])')
63+
.first();
64+
await filledCell.hover();
65+
const tooltip = page.locator('main section [class*="punch-tooltip"]');
66+
await expect(tooltip).toBeVisible();
67+
await expect(tooltip).toContainText("posts");
68+
69+
// contribution calendar shows the latest year (2023) with one cell per day
70+
await expect(
71+
page.getByText("Contributions (2023, Asia/Tokyo)")
72+
).toBeVisible();
73+
await expect(page.locator("main section [data-date]")).toHaveCount(365);
74+
75+
// NOTE: the mock's posts are all in 2023; only years which have posts are listed.
76+
const yearOptions = await yearDropdown.locator("option").allTextContents();
77+
expect(yearOptions).toEqual(["ALL", "2023"]);
78+
79+
// all years aggregation is selected by default
80+
await expect(yearDropdown).toHaveValue("all");
81+
82+
// switch to a single year and the charts should still render
83+
await yearDropdown.selectOption({ value: "2023" });
84+
await expect(page.locator("main section svg").first()).toBeVisible();
85+
await expect(page.locator("main section [data-count]")).toHaveCount(168);
86+
87+
// switch the timezone (config.e2e.js: Asia/Tokyo + UTC) and re-aggregate
88+
const timeZoneOptions = await timeZoneDropdown
89+
.locator("option")
90+
.allTextContents();
91+
expect(timeZoneOptions).toEqual(["Asia/Tokyo", "UTC"]);
92+
await timeZoneDropdown.selectOption({ value: "UTC" });
93+
await expect(page.getByText("Posts by Hour (UTC)")).toBeVisible();
94+
await expect(page.getByText("Punch Card (UTC)")).toBeVisible();
95+
await expect(page.getByText("Contributions (2023, UTC)")).toBeVisible();
96+
await expect(page.locator("main section [data-count]")).toHaveCount(168);
97+
await expect(page.locator("main section [data-date]")).toHaveCount(365);
98+
99+
const screenshot = await page.screenshot({ fullPage: true });
100+
await testInfo.attach("screenshot", {
101+
body: screenshot,
102+
contentType: "image/png"
103+
});
104+
});
105+
106+
test("should display top tags pie chart with linked legend", async ({
107+
page
108+
}) => {
109+
// NOTE: the mock's tags fixture is: diary(291), Scala(96), Akka(9), Rust(4), Android(2)
110+
await expect(page.locator("main section .recharts-pie")).toHaveCount(1);
111+
112+
const tagLinks = page.locator("main section a[data-tag]");
113+
await expect(tagLinks).toHaveCount(5);
114+
115+
const first = tagLinks.first();
116+
await expect(first).toHaveText("diary");
117+
await expect(first).toHaveAttribute("href", "/tags/diary");
118+
119+
const scala = page.locator('main section a[data-tag="scala"]');
120+
await expect(scala).toHaveAttribute("href", "/tags/scala");
121+
122+
// all 5 tags are within the topTags limit (7), so no "Others" entry
123+
await expect(page.getByText("Others")).toHaveCount(0);
124+
});
125+
});

0 commit comments

Comments
 (0)