0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-14 13:58:53 +08:00
discourse/plugins/poll/test/javascripts/component/poll-test.gjs
Gabriel Grubba 1e48a5d535
FIX: Respect poll result visibility for ranked-choice outcomes (#40578)
Ranked-choice polls always serialized `ranked_choice_outcome` (the
computed winner and round-by-round activity) regardless of the poll's
`results` setting, unlike vote counts and voter lists, which are gated
by `Poll#can_see_results?`.


The outcome was therefore displayed to users who should not yet see
results on `on_vote`, `on_close`, and `staff_only` polls — through the
topic view, the vote/remove-vote responses, and the MessageBus
broadcast.

- Gate `ranked_choice_outcome` in `PollSerializer` behind
`can_see_results?`, matching the other result fields.
- Stop manually re-appending the outcome in `vote`/`remove_vote` so the
serializer's gating is authoritative.
- Serialize the MessageBus payload as an anonymous user so the
topic-wide broadcast only carries data any viewer may see, matching
`PollsUpdater.publish_changes`.
- Don't render an empty results panel to a non-staff voter on a
`staff_only` poll; show the ballot and the staff-only notice instead.



Relates to /t/-/185090
And Patch 998

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 12:04:38 -03:00

401 lines
10 KiB
Text
Vendored

import EmberObject from "@ember/object";
import { trackedObject } from "@ember/reactive/collections";
import { click, render } from "@ember/test-helpers";
import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import pretender, { response } from "discourse/tests/helpers/create-pretender";
import { i18n } from "discourse-i18n";
import Poll from "discourse/plugins/poll/discourse/components/poll";
let requests = 0;
module("Component | Poll", function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
pretender.put("/polls/vote", () => {
++requests;
return response({
poll: {
name: "poll",
type: "regular",
status: "open",
results: "always",
options: [
{
id: "1f972d1df351de3ce35a787c89faad29",
html: "yes",
votes: 1,
},
{
id: "d7ebc3a9beea2e680815a1e4f57d6db6",
html: "no",
votes: 0,
},
],
voters: 1,
chart_type: "bar",
},
vote: ["1f972d1df351de3ce35a787c89faad29"],
});
});
});
test("valid ranks with which you can vote", async function (assert) {
this.setProperties({
post: EmberObject.create({
id: 42,
topic: {
archived: false,
},
user_id: 29,
polls_votes: {
poll: [
{
digest: "1f972d1df351de3ce35a787c89faad29",
rank: 1,
},
{
digest: "d7ebc3a9beea2e680815a1e4f57d6db6",
rank: 2,
},
{
digest: "6c986ebcde3d5822a6e91a695c388094",
rank: 3,
},
],
},
}),
poll: trackedObject({
name: "poll",
type: "ranked_choice",
status: "open",
results: "on_close",
options: [
{
id: "1f972d1df351de3ce35a787c89faad29",
html: "this",
votes: 0,
rank: 1,
},
{
id: "d7ebc3a9beea2e680815a1e4f57d6db6",
html: "that",
votes: 0,
rank: 2,
},
{
id: "6c986ebcde3d5822a6e91a695c388094",
html: "other",
votes: 0,
rank: 3,
},
],
voters: 0,
chart_type: "bar",
}),
});
await render(
<template><Poll @post={{this.post}} @poll={{this.poll}} /></template>
);
assert.dom(".poll-buttons .cast-votes:disabled").doesNotExist();
assert.dom(".poll-buttons .cast-votes").exists();
});
test("invalid ranks with which you cannot vote", async function (assert) {
this.setProperties({
post: EmberObject.create({
id: 42,
topic: {
archived: false,
},
user_id: 29,
}),
poll: trackedObject({
name: "poll",
type: "ranked_choice",
status: "open",
results: "always",
options: [
{
id: "1f972d1df351de3ce35a787c89faad29",
html: "this",
votes: 0,
rank: 0,
},
{
id: "d7ebc3a9beea2e680815a1e4f57d6db6",
html: "that",
votes: 0,
rank: 0,
},
{
id: "6c986ebcde3d5822a6e91a695c388094",
html: "other",
votes: 0,
rank: 0,
},
],
voters: 0,
chart_type: "bar",
}),
});
await render(
<template><Poll @post={{this.post}} @poll={{this.poll}} /></template>
);
await click(
".ranked-choice-poll-option[data-poll-option-id='1f972d1df351de3ce35a787c89faad29'] button",
"open dropdown"
);
assert
.dom(".dropdown-menu__item:nth-child(2)")
.hasText(`1 ${i18n("poll.options.ranked_choice.highest_priority")}`);
await click(
".dropdown-menu__item:nth-child(2) button",
"select 1st priority"
);
assert.dom(".poll-buttons .cast-votes:disabled").doesNotExist();
assert.dom(".poll-buttons .cast-votes").exists();
await click(
".ranked-choice-poll-option[data-poll-option-id='1f972d1df351de3ce35a787c89faad29'] button",
"open dropdown"
);
await click(".dropdown-menu__item:nth-child(1) button", "select Abstain");
assert.dom(".poll-buttons .cast-votes:disabled").exists();
});
test("shows vote", async function (assert) {
this.setProperties({
post: EmberObject.create({
id: 42,
topic: {
archived: false,
},
user_id: 29,
}),
poll: trackedObject({
name: "poll",
type: "regular",
status: "closed",
results: "always",
options: [
{ id: "1f972d1df351de3ce35a787c89faad29", html: "yes", votes: 1 },
{ id: "d7ebc3a9beea2e680815a1e4f57d6db6", html: "no", votes: 0 },
],
voters: 1,
chart_type: "bar",
}),
});
await render(
<template><Poll @post={{this.post}} @poll={{this.poll}} /></template>
);
assert.dom(".results li:nth-of-type(1) .option p").hasText("100% yes");
assert.dom(".results li:nth-of-type(2) .option p").hasText("0% no");
});
test("does not show results after voting when results are to be shown only on closed", async function (assert) {
this.setProperties({
post: EmberObject.create({
id: 42,
topic: {
archived: false,
},
user_id: 29,
}),
poll: trackedObject({
name: "poll",
type: "regular",
status: "open",
results: "on_close",
options: [
{ id: "1f972d1df351de3ce35a787c89faad29", html: "yes" },
{ id: "d7ebc3a9beea2e680815a1e4f57d6db6", html: "no" },
],
voters: 1,
chart_type: "bar",
}),
});
await render(
<template><Poll @post={{this.post}} @poll={{this.poll}} /></template>
);
assert.dom("ul.options").exists("options are shown");
assert.dom("ul.results").doesNotExist("results are not shown");
});
test("does not render an empty ranked choice outcome to a non-staff voter on staff_only polls", async function (assert) {
this.setProperties({
post: EmberObject.create({
id: 42,
topic: {
archived: false,
},
user_id: 29,
polls_votes: {
poll: [
{ digest: "1f972d1df351de3ce35a787c89faad29", rank: 1 },
{ digest: "d7ebc3a9beea2e680815a1e4f57d6db6", rank: 2 },
],
},
}),
poll: trackedObject({
name: "poll",
type: "ranked_choice",
status: "open",
results: "staff_only",
options: [
{ id: "1f972d1df351de3ce35a787c89faad29", html: "this", rank: 1 },
{ id: "d7ebc3a9beea2e680815a1e4f57d6db6", html: "that", rank: 2 },
],
voters: 1,
chart_type: "bar",
// no ranked_choice_outcome — the server withholds it from non-staff
}),
});
await render(
<template><Poll @post={{this.post}} @poll={{this.poll}} /></template>
);
assert
.dom("table.poll-results-ranked-choice")
.doesNotExist("the empty outcome table is not rendered");
assert
.dom(".results-staff-only")
.exists("the staff-only results notice is shown instead");
assert
.dom(".ranked-choice-poll-option")
.exists("the ballot options are shown to the voter");
});
test("can vote", async function (assert) {
this.setProperties({
post: EmberObject.create({
id: 42,
topic: {
archived: false,
},
user_id: 29,
}),
poll: trackedObject({
name: "poll",
type: "regular",
status: "open",
results: "always",
options: [
{ id: "1f972d1df351de3ce35a787c89faad29", html: "yes", votes: 0 },
{ id: "d7ebc3a9beea2e680815a1e4f57d6db6", html: "no", votes: 0 },
],
voters: 0,
chart_type: "bar",
}),
});
await render(
<template><Poll @post={{this.post}} @poll={{this.poll}} /></template>
);
requests = 0;
await click(
"li[data-poll-option-id='1f972d1df351de3ce35a787c89faad29'] button"
);
assert.strictEqual(requests, 1);
assert.dom(".chosen").exists({ count: 1 });
await click(".toggle-results");
assert
.dom("li[data-poll-option-id='1f972d1df351de3ce35a787c89faad29']")
.exists({ count: 1 });
});
test("cannot vote if not member of the right group", async function (assert) {
this.setProperties({
post: EmberObject.create({
id: 42,
topic: {
archived: false,
},
user_id: 29,
}),
poll: trackedObject({
name: "poll",
type: "regular",
status: "open",
results: "always",
options: [
{ id: "1f972d1df351de3ce35a787c89faad29", html: "yes", votes: 0 },
{ id: "d7ebc3a9beea2e680815a1e4f57d6db6", html: "no", votes: 0 },
],
voters: 0,
chart_type: "bar",
groups: "foo",
}),
});
await render(
<template><Poll @post={{this.post}} @poll={{this.poll}} /></template>
);
requests = 0;
await click(
"li[data-poll-option-id='1f972d1df351de3ce35a787c89faad29'] button"
);
assert
.dom(".poll-container .alert")
.hasText(i18n("poll.results.groups.title", { groups: "foo" }));
assert.strictEqual(requests, 0);
assert.dom(".chosen").doesNotExist();
});
test("voting on a multiple poll with no min attribute", async function (assert) {
this.setProperties({
post: EmberObject.create({
id: 42,
topic: {
archived: false,
},
user_id: 29,
}),
poll: EmberObject.create({
name: "poll",
type: "multiple",
status: "open",
results: "always",
max: 2,
options: [
{ id: "1f972d1df351de3ce35a787c89faad29", html: "yes", votes: 0 },
{ id: "d7ebc3a9beea2e680815a1e4f57d6db6", html: "no", votes: 0 },
],
voters: 0,
chart_type: "bar",
}),
});
await render(
<template><Poll @post={{this.post}} @poll={{this.poll}} /></template>
);
assert.dom(".poll-buttons .cast-votes").isDisabled();
await click(
"li[data-poll-option-id='1f972d1df351de3ce35a787c89faad29'] button"
);
await click(".poll-buttons .cast-votes");
assert.dom(".chosen").exists();
});
});