Files
Ivan Batsulin 76f8441b45 feat: add warning news type and Firefox performance notice (#3680)
## Description:
Adds a new `warning` news type to the news banner system and uses it to
display a Firefox performance notice.

Changes:
- Added `warning` type with red styling to `NewsBox.ts`
- Added `news_box.warning` key (`"WARNING"`) to `en.json`
- Added Firefox performance notice to `resources/news.json` using the
new `warning` type
- Added `news_box.*` dynamic key pattern to `TranslationSystem.test.ts`
to fix unused key detection

## Please complete the following:
- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## UI change:
<img width="2101" height="1770" alt="CleanShot 2026-04-16 at 15 04
35@2x"
src="https://github.com/user-attachments/assets/7a8b9290-4216-4799-b271-606afd9b8723"
/>



## Please put your Discord username so you can be contacted if a bug or
regression is found:
fghjk_60845
2026-04-16 16:53:38 -07:00

93 lines
2.8 KiB
TypeScript

import newsItems from "../../../resources/news.json";
import {
getVisibleNewsItems,
NewsItem,
} from "../../../src/client/components/NewsBox";
const DISMISSED_NEWS_KEY = "dismissedNewsItems";
const allItems = newsItems as NewsItem[];
function createMockLocalStorage(): Storage {
let store: Record<string, string> = {};
return {
getItem: (key: string) => store[key] ?? null,
setItem: (key: string, value: string) => {
store[key] = String(value);
},
removeItem: (key: string) => {
delete store[key];
},
clear: () => {
store = {};
},
get length() {
return Object.keys(store).length;
},
key: (index: number) => Object.keys(store)[index] ?? null,
};
}
describe("NewsBox", () => {
beforeEach(() => {
vi.stubGlobal("localStorage", createMockLocalStorage());
});
afterEach(() => {
vi.unstubAllGlobals();
});
describe("getVisibleNewsItems", () => {
it("returns all items when none are dismissed", () => {
const items = getVisibleNewsItems(allItems);
expect(items.length).toBe(newsItems.length);
});
it("filters out dismissed items", () => {
const items = getVisibleNewsItems(allItems);
const firstId = items[0].id;
localStorage.setItem(DISMISSED_NEWS_KEY, JSON.stringify([firstId]));
const filtered = getVisibleNewsItems(allItems);
expect(filtered.find((i) => i.id === firstId)).toBeUndefined();
expect(filtered.length).toBe(items.length - 1);
});
it("returns empty when all items are dismissed", () => {
const allIds = allItems.map((i) => i.id);
localStorage.setItem(DISMISSED_NEWS_KEY, JSON.stringify(allIds));
const items = getVisibleNewsItems(allItems);
expect(items.length).toBe(0);
});
});
describe("news items structure", () => {
it("each item has required fields", () => {
const items = getVisibleNewsItems(allItems);
for (const item of items) {
expect(item.id).toBeDefined();
expect(typeof item.id).toBe("string");
expect(item.title).toBeDefined();
expect(typeof item.title).toBe("string");
const hasDescription =
item.description !== undefined ||
item.descriptionTranslationKey !== undefined;
expect(hasDescription).toBe(true);
expect(item.type).toBeDefined();
expect(["tournament", "tutorial", "announcement", "warning"]).toContain(
item.type,
);
}
});
it("each item has a unique id", () => {
const items = getVisibleNewsItems(allItems);
const ids = items.map((i) => i.id);
expect(new Set(ids).size).toBe(ids.length);
});
it("contains a tournament entry", () => {
const items = getVisibleNewsItems(allItems);
expect(items.some((i) => i.type === "tournament")).toBe(true);
});
});
});