0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-12 02:52:01 +08:00
discourse/plugins/discourse-solved/test/javascripts/integration/support-test.gjs
Osama Sayegh e0ae5b1bae
FEATURE: link Topic outcomes to the filtered topic list (#42132)
Resolved, In progress, and Unanswered in the Support section's Topic
outcomes chart are now links to topic lists (via the `/filter` route) ,
filtered to the matching topics for the dashboard's current date range
and category selection.
2026-07-30 06:06:35 +03:00

427 lines
12 KiB
Text
Vendored

import { render } from "@ember/test-helpers";
import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import selectKit from "discourse/tests/helpers/select-kit-helper";
import SupportSection from "discourse/plugins/discourse-solved/admin/components/dashboard/support";
function buildData(overrides = {}) {
return {
category_options: [],
kpis: {
resolution_rate: {
value: 72,
previous_value: 69,
report_type: "accepted_solutions",
report_query: { start_date: "2026-04-01", end_date: "2026-04-30" },
},
staff_involvement: { value: 19, previous_value: 25 },
avg_first_reply: { value: 11100, previous_value: 10620 },
},
headline: {
key: "healthy",
resolution_rate: 72,
resolution_direction: "up",
answerers_focus: "members",
answerers_share: 82,
first_reply_seconds: 11100,
first_reply_direction: "slower",
first_reply_delta_seconds: 480,
unanswered_count: 3,
},
topic_outcomes: { resolved: 1, in_progress: 0, unanswered: 3 },
whos_answering: {
rows: [{ type: "staff", count: 1, share: 100 }],
total: 1,
},
response_time_distribution: {
buckets: [
{ key: "lt_1h", count: 1, share: 100 },
{ key: "1_4h", count: 0, share: 0 },
{ key: "4_24h", count: 0, share: 0 },
{ key: "gt_24h", count: 0, share: 0 },
],
trend: { direction: "flat", seconds: 0 },
},
...overrides,
};
}
const startDate = new Date("2026-04-01");
const endDate = new Date("2026-04-30");
module("Integration | Component | Dashboard | Support", function (hooks) {
setupRenderingTest(hooks);
test("renders a positive resolution-rate delta", async function (assert) {
const data = buildData();
await render(
<template>
<SupportSection
@data={{data}}
@startDate={{startDate}}
@endDate={{endDate}}
/>
</template>
);
assert
.dom(".db-section__metric:first-child .db-delta.--pos")
.hasText("+3%");
});
test("renders a negative resolution-rate delta", async function (assert) {
const data = buildData({
kpis: {
resolution_rate: { value: 34, previous_value: 43, report_query: {} },
staff_involvement: { value: 81, previous_value: 62 },
avg_first_reply: { value: 51720, previous_value: 20520 },
},
});
await render(
<template>
<SupportSection
@data={{data}}
@startDate={{startDate}}
@endDate={{endDate}}
/>
</template>
);
assert
.dom(".db-section__metric:first-child .db-delta.--neg")
.hasText("-9%");
});
test("renders a stable tag for metrics that did not change", async function (assert) {
const data = buildData({
kpis: {
resolution_rate: { value: 50, previous_value: 50, report_query: {} },
staff_involvement: { value: 20, previous_value: 20 },
avg_first_reply: { value: 11100, previous_value: 11100 },
},
});
await render(
<template>
<SupportSection
@data={{data}}
@startDate={{startDate}}
@endDate={{endDate}}
/>
</template>
);
assert.dom(".db-section__metrics .db-pill").exists({ count: 3 });
assert.dom(".db-section__metrics .db-delta").doesNotExist();
});
test("shows no delta or stable tag when the previous period has no data", async function (assert) {
const data = buildData({
kpis: {
resolution_rate: { value: 40, previous_value: null, report_query: {} },
staff_involvement: { value: 30, previous_value: null },
avg_first_reply: { value: 11100, previous_value: null },
},
});
await render(
<template>
<SupportSection
@data={{data}}
@startDate={{startDate}}
@endDate={{endDate}}
/>
</template>
);
assert.dom(".db-section__metrics .db-delta").doesNotExist();
assert.dom(".db-section__metrics .db-pill").doesNotExist();
});
test("composes a trend-aware headline summary from each metric's direction", async function (assert) {
const positive = buildData();
await render(
<template>
<SupportSection
@data={{positive}}
@startDate={{startDate}}
@endDate={{endDate}}
/>
</template>
);
assert.dom(".db-section__subintro p").includesText("climbed to 72%");
assert
.dom(".db-section__subintro p")
.includesText("Members are handling 82%");
const negative = buildData({
headline: {
key: "struggling",
resolution_rate: 34,
resolution_direction: "down",
answerers_focus: "staff",
answerers_share: 81,
first_reply_seconds: 51720,
first_reply_direction: "slower",
first_reply_delta_seconds: 31200,
unanswered_count: 47,
},
});
await render(
<template>
<SupportSection
@data={{negative}}
@startDate={{startDate}}
@endDate={{endDate}}
/>
</template>
);
assert.dom(".db-section__subintro p").includesText("dropped to 34%");
assert
.dom(".db-section__subintro p")
.includesText("47 topics from this period still have zero replies");
});
test("shows a placeholder when the average first reply is unknown", async function (assert) {
const data = buildData({
kpis: {
resolution_rate: { value: 0, previous_value: 0, report_query: {} },
staff_involvement: { value: 0, previous_value: 0 },
avg_first_reply: { value: null, previous_value: null },
},
});
await render(
<template>
<SupportSection
@data={{data}}
@startDate={{startDate}}
@endDate={{endDate}}
/>
</template>
);
assert.dom(".db-section__metrics").exists();
assert.dom(".db-section__metrics").includesText("—");
});
test("shows the category filter only when more than one support category exists", async function (assert) {
const single = buildData({
category_options: [{ id: 1, name: "Support" }],
});
await render(
<template>
<SupportSection
@data={{single}}
@startDate={{startDate}}
@endDate={{endDate}}
/>
</template>
);
assert.dom(".db-support__filter").doesNotExist("hidden with one category");
const multiple = buildData({
category_options: [
{ id: 1, name: "Support" },
{ id: 2, name: "Help" },
],
});
await render(
<template>
<SupportSection
@data={{multiple}}
@startDate={{startDate}}
@endDate={{endDate}}
/>
</template>
);
assert.dom(".db-support__filter").exists("shown with multiple categories");
assert.dom(".multiple-categories-selector").exists();
});
test("prefills the selector with the persisted category selection", async function (assert) {
const data = buildData({
category_options: [
{ id: 1, name: "Support" },
{ id: 2, name: "Help" },
],
category_ids: [1, 2],
});
await render(
<template>
<SupportSection
@data={{data}}
@startDate={{startDate}}
@endDate={{endDate}}
/>
</template>
);
assert.strictEqual(
selectKit(".multiple-categories-selector").header().value(),
"1,2"
);
});
test("links Topic outcomes rows to the filtered topic list, scoped to the date range and without a category restriction when solved is allowed on all topics", async function (assert) {
this.siteSettings.allow_solved_on_all_topics = true;
const data = buildData({ category_options: [], category_ids: [] });
await render(
<template>
<SupportSection
@data={{data}}
@startDate={{startDate}}
@endDate={{endDate}}
/>
</template>
);
const dateRange = `created-after:${moment(startDate).format(
"YYYY-MM-DD"
)} created-before:${moment(endDate).format("YYYY-MM-DD")}`;
assert
.dom(".db-support-outcomes__row:nth-child(1) a")
.hasAttribute(
"href",
`/filter?q=${encodeURIComponent(`status:solved ${dateRange}`)}`,
"resolved links to status:solved with the current date range"
);
assert
.dom(".db-support-outcomes__row:nth-child(2) a")
.hasAttribute(
"href",
`/filter?q=${encodeURIComponent(
`status:unsolved posts-min:2 ${dateRange}`
)}`,
"in progress links to status:unsolved posts-min:2 with the current date range"
);
assert
.dom(".db-support-outcomes__row:nth-child(3) a")
.hasAttribute(
"href",
`/filter?q=${encodeURIComponent(
`status:unsolved status:noreplies ${dateRange}`
)}`,
"unanswered links to status:unsolved status:noreplies with the current date range"
);
});
test("restricts Topic outcomes links to every support category when none is selected and solved is not allowed on all topics", async function (assert) {
this.siteSettings.allow_solved_on_all_topics = false;
const data = buildData({
category_options: [
{ id: 1, name: "Bug" },
{ id: 2, name: "Feature" },
],
category_ids: [],
});
await render(
<template>
<SupportSection
@data={{data}}
@startDate={{startDate}}
@endDate={{endDate}}
/>
</template>
);
assert
.dom(".db-support-outcomes__row:nth-child(1) a")
.hasAttribute(
"href",
/category%3Abug%2Cfeature/,
"restricts to every support category when none is selected"
);
});
test("restricts Topic outcomes links to the selected category when one is applied", async function (assert) {
this.siteSettings.allow_solved_on_all_topics = false;
const data = buildData({
category_options: [
{ id: 1, name: "Bug" },
{ id: 3, name: "Meta" },
],
category_ids: [3],
});
await render(
<template>
<SupportSection
@data={{data}}
@startDate={{startDate}}
@endDate={{endDate}}
/>
</template>
);
assert
.dom(".db-support-outcomes__row:nth-child(1) a")
.hasAttribute(
"href",
/category%3Ameta(?!%2C)/,
"restricts to only the selected category"
);
});
test("exposes Topic outcomes rows as focusable links, keeping the bar graphic hidden from assistive tech", async function (assert) {
const data = buildData();
await render(
<template>
<SupportSection
@data={{data}}
@startDate={{startDate}}
@endDate={{endDate}}
/>
</template>
);
assert
.dom(".db-support-outcomes__bars")
.doesNotHaveAttribute("role", "no longer collapsed into a single img");
assert
.dom(".db-support-outcomes__label")
.exists({ count: 3 }, "each row renders a focusable label link");
assert
.dom(".db-support-outcomes__row:nth-child(1) a")
.hasAttribute(
"href",
`/filter?q=${encodeURIComponent(
`status:solved created-after:${moment(startDate).format(
"YYYY-MM-DD"
)} created-before:${moment(endDate).format("YYYY-MM-DD")}`
)}`,
"resolved links to its full filter query"
);
assert
.dom(".db-support-outcomes__track")
.hasAttribute(
"aria-hidden",
"true",
"the decorative bar is hidden from assistive tech"
);
assert
.dom(".db-support-outcomes__share")
.doesNotHaveAttribute(
"aria-hidden",
"the count stays exposed to assistive tech"
);
});
});