0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-08 17:53:55 +08:00
discourse/plugins/discourse-ai/test/javascripts/integration/components/ai-translations-test.gjs
Kris c43352e52e
UX: improve AI translation configuration (#42241)
This makes enabling AI translations clearer by reordering the settings
to put the toggle last (language must be selected first) and adding a
tooltip when the feature can not be enabled.

Before:
<img width="1000" alt="image"
src="https://github.com/user-attachments/assets/211ea1ed-0b69-4a36-b8b4-4caf5feca691"
/>


After:
<img width="1000" alt="image"
src="https://github.com/user-attachments/assets/cc68fd3b-db7c-470b-b64f-63cff392d2e6"
/>


<img width="1000" alt="image"
src="https://github.com/user-attachments/assets/0539e624-7d7d-45ca-a6b8-bb3ac532fc28"
/>
2026-07-31 15:51:31 -04:00

231 lines
6.7 KiB
Text
Vendored

import Service from "@ember/service";
import { click, find, render, settled, waitUntil } 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 AiTranslations from "discourse/plugins/discourse-ai/discourse/components/ai-translations";
class AiCreditsStub extends Service {
async getFeatureCreditStatus() {
return null;
}
}
const overview = {
cached_at: "2026-07-23T09:00:00Z",
targets: ["post", "topic", "category", "tag"].map((target_type) => ({
target_type,
total_count: 10,
translated_count: 5,
needs_language_detection_count: 2,
})),
};
function detail(target_type, translated_count) {
return {
target_type,
cached_at: "2026-07-23T09:00:00Z",
locales: [
{
locale: "en",
translated_count,
pending_count: 10 - translated_count,
eligible_count: 10,
},
{
locale: "fr",
translated_count,
pending_count: 10 - translated_count,
eligible_count: 10,
},
],
};
}
module("Integration | Component | AiTranslations", function (hooks) {
setupRenderingTest(hooks, { stubRouter: true });
hooks.beforeEach(function () {
this.owner.unregister("service:ai-credits");
this.owner.register("service:ai-credits", AiCreditsStub);
this.siteSettings.content_localization_supported_locales = "en|fr";
this.siteSettings.available_locales = [
{ name: "English", value: "en" },
{ name: "French", value: "fr" },
];
this.model = {
enabled: true,
translation_enabled: true,
no_locales_configured: false,
backfill_enabled: false,
category_scope: "public",
category_ids: [],
hourly_rate: 0,
translation_id: 6,
};
pretender.get(
"/admin/plugins/discourse-ai/ai-translations/progress.json",
() => response(overview)
);
});
test("keeps the current detail table open while another target loads", async function (assert) {
let resolveTopic;
pretender.get(
"/admin/plugins/discourse-ai/ai-translations/progress/post.json",
() => response(detail("post", 4))
);
pretender.get(
"/admin/plugins/discourse-ai/ai-translations/progress/topic.json",
() =>
new Promise((resolve) => {
resolveTopic = () => resolve(response(detail("topic", 7)));
})
);
await render(<template><AiTranslations @model={{this.model}} /></template>);
await click(
".ai-translation-model-progress-overview-card[data-target-type='post']"
);
find(
".ai-translation-model-progress-overview-card[data-target-type='topic']"
).click();
await waitUntil(() => find(".ai-translation-model-progress-detail-state"));
assert
.dom(".ai-translation-model-progress-detail")
.exists("the current detail table remains mounted");
assert
.dom(".ai-translation-model-progress-detail-state.--overlay")
.exists("the loading state overlays the current table");
resolveTopic();
await settled();
assert
.dom(".ai-translation-locale-progress__translated-value")
.hasText("7", "the newly loaded detail replaces the previous table");
});
test("does not reuse a detail response that finishes after disabling translations", async function (assert) {
let resolveFirstRequest;
let detailRequestCount = 0;
pretender.get(
"/admin/plugins/discourse-ai/ai-translations/progress/post.json",
() => {
detailRequestCount += 1;
if (detailRequestCount === 1) {
return new Promise((resolve) => {
resolveFirstRequest = () => resolve(response(detail("post", 4)));
});
}
return response(detail("post", 8));
}
);
pretender.put("/admin/site_settings/ai_translation_enabled", () =>
response({})
);
pretender.put("/admin/site_settings/content_localization_enabled", () =>
response({})
);
await render(<template><AiTranslations @model={{this.model}} /></template>);
find(
".ai-translation-model-progress-overview-card[data-target-type='post']"
).click();
await waitUntil(() => resolveFirstRequest);
find(
".ai-translations__toggle-container .d-toggle-switch__checkbox"
).click();
await waitUntil(() => !find(".ai-translations__overview"));
resolveFirstRequest();
await settled();
await click(
".ai-translations__toggle-container .d-toggle-switch__checkbox"
);
await click(
".ai-translation-model-progress-overview-card[data-target-type='post']"
);
assert.strictEqual(
detailRequestCount,
2,
"the stale response is not retained in the frontend cache"
);
assert
.dom(".ai-translation-locale-progress__translated-value")
.hasText("8", "the refreshed response is rendered");
});
test("disables the toggle as soon as every supported language is removed", async function (assert) {
this.model = {
...this.model,
enabled: false,
translation_enabled: false,
};
pretender.put(
"/admin/site_settings/content_localization_supported_locales",
() => response({})
);
await render(<template><AiTranslations @model={{this.model}} /></template>);
const toggle =
".ai-translations__toggle-container .d-toggle-switch__checkbox";
assert
.dom(toggle)
.isNotDisabled("the toggle is usable while locales exist");
await click(".ai-translations__locale-input-row .setting-controls__undo");
assert
.dom(toggle)
.isDisabled("clearing the locale selection disables the toggle");
assert.dom(".ai-translations__toggle-disabled-tooltip").exists();
await click(".ai-translations__locale-input-row .setting-controls__ok");
assert
.dom(toggle)
.isDisabled("the toggle stays disabled after saving no locales");
});
test("shows the fixed backfill start date", async function (assert) {
this.owner.lookup("service:router").urlFor = () =>
"/admin/plugins/discourse-ai/ai-features/6/edit";
const backfillStartDate = "2026-07-01";
const formattedDate = new Date(backfillStartDate).toLocaleDateString(
undefined,
{
year: "numeric",
month: "long",
day: "numeric",
timeZone: "UTC",
}
);
this.model = {
...this.model,
backfill_enabled: true,
backfill_start_date: backfillStartDate,
hourly_rate: 50,
};
await render(<template><AiTranslations @model={{this.model}} /></template>);
assert
.dom(".ai-translations__stat-label")
.includesText(
formattedDate,
"the status uses the configured fixed cutoff date"
);
});
});