diff --git a/src/clock.ts b/src/clock.ts index f1066db..0a38342 100644 --- a/src/clock.ts +++ b/src/clock.ts @@ -4,6 +4,9 @@ export const isChristmas = (clock: Clock = SystemClock) => clock.now().month() = export const isHalloween = (clock: Clock = SystemClock) => clock.now().month() == 9 && clock.now().date() == 31 export const isHoli = (clock: Clock = SystemClock) => ["2022/03/18", "2023/03/07", "2024/03/25", "2025/03/14"].map(dayjs).find(it => it.isSame(clock.now())) != undefined export const isCNY = (clock: Clock = SystemClock) => ["2022/02/01", "2023/01/22", "2024/02/10", "2025/02/29"].map(dayjs).find(it => it.isSame(clock.now())) != undefined +export const isCNY_2022 = (clock: Clock = SystemClock) => clock.now().isSame(dayjs("2022/02/01")) +export const isCNY_2023 = (clock: Clock = SystemClock) => clock.now().isSame(dayjs("2023/01/22")) +export const isCNY_2024 = (clock: Clock = SystemClock) => clock.now().isSame(dayjs("2024/02/10")) export interface Clock { now(): Dayjs; diff --git a/src/icon.ts b/src/icon.ts index 9d53dd8..c9d0469 100644 --- a/src/icon.ts +++ b/src/icon.ts @@ -5,19 +5,15 @@ import fs from "fs"; import { Clock, isChristmas, - isCNY, + isCNY_2022, + isCNY_2023, + isCNY_2024, isHalloween, isHoli, SystemClock, } from "./clock"; import path from "path"; -export type Transformation = { - viewPortIncreasePercent: number | undefined; - backgroundColor: string | undefined; - foregroundColor: string | undefined; -}; - const SVG_NS = { svg: "http://www.w3.org/2000/svg", }; @@ -47,55 +43,77 @@ class ViewBox { `${this.minX} ${this.minY} ${this.width} ${this.height}`; } +export type IconFeatures = { + viewPortIncreasePercent: number | undefined; + backgroundColor: string | undefined; + foregroundColor: string | undefined; +}; + +export type IconSpec = { + svg: string | undefined; + features: Partial | undefined; +}; + export interface Icon { - with(newTransformation: Partial): Icon; + with(spec: Partial): Icon; + apply(transformer: Transformer): Icon; } -export class ColorOverridingIcon implements Icon { - rule: () => Boolean; - newColors: () => Partial< - Pick - >; - icon: Icon; +export type Transformer = (icon: Icon) => Icon; - constructor( - icon: Icon, - rule: () => Boolean, - newColors: () => Partial< - Pick - > - ) { - this.icon = icon; - this.rule = rule; - this.newColors = newColors; - } +export function transform(spec: Partial): Transformer { + return (icon: Icon) => + icon.with({ + ...spec, + features: { ...spec.features }, + }); +} - public with = (transformation: Partial) => - this.rule() - ? this.icon.with({ ...transformation, ...this.newColors() }) - : this.icon.with(transformation); +export function features(features: Partial): Transformer { + return (icon: Icon) => icon.with({ features }); +} - public toString = () => this.with({}).toString(); +export function maybeTransform(rule: () => Boolean, transformer: Transformer) { + return (icon: Icon) => (rule() ? transformer(icon) : icon); +} + +export function allOf(...transformers: Transformer[]): Transformer { + return (icon: Icon): Icon => + _.inject( + transformers, + (current: Icon, transformer: Transformer) => transformer(current), + icon + ); } export class SvgIcon implements Icon { - private svg: string; - private transformation: Transformation; + svg: string; + features: IconFeatures; constructor( svg: string, - transformation: Transformation = { + features: Partial = { viewPortIncreasePercent: undefined, backgroundColor: undefined, foregroundColor: undefined, } ) { this.svg = svg; - this.transformation = transformation; + this.features = { + viewPortIncreasePercent: undefined, + backgroundColor: undefined, + foregroundColor: undefined, + ...features, + }; } - public with = (newTransformation: Partial) => - new SvgIcon(this.svg, { ...this.transformation, ...newTransformation }); + public apply = (transformer: Transformer): Icon => transformer(this); + + public with = (spec: Partial) => + new SvgIcon(spec.svg || this.svg, { + ...this.features, + ...spec.features, + }); public toString = () => { const xml = libxmljs.parseXmlString(this.svg, { @@ -105,30 +123,28 @@ export class SvgIcon implements Icon { const viewBoxAttr = xml.get("//svg:svg/@viewBox", SVG_NS) as Attribute; let viewBox = new ViewBox(viewBoxAttr.value()); if ( - this.transformation.viewPortIncreasePercent && - this.transformation.viewPortIncreasePercent > 0 + this.features.viewPortIncreasePercent && + this.features.viewPortIncreasePercent > 0 ) { - viewBox = viewBox.increasePercent( - this.transformation.viewPortIncreasePercent - ); + viewBox = viewBox.increasePercent(this.features.viewPortIncreasePercent); viewBoxAttr.value(viewBox.toString()); } - if (this.transformation.backgroundColor) { + if (this.features.backgroundColor) { (xml.get("//svg:svg/*[1]", SVG_NS) as Element).addPrevSibling( new Element(xml, "rect").attr({ x: `${viewBox.minX}`, y: `${viewBox.minY}`, width: `${Math.abs(viewBox.minX) + viewBox.width}`, height: `${Math.abs(viewBox.minY) + viewBox.height}`, - fill: this.transformation.backgroundColor, + fill: this.features.backgroundColor, }) ); } - if (this.transformation.foregroundColor) { + if (this.features.foregroundColor) { (xml.find("//svg:path", SVG_NS) as Element[]).forEach((path) => { if (path.attr("fill")) - path.attr({ stroke: this.transformation.foregroundColor! }); - else path.attr({ fill: this.transformation.foregroundColor! }); + path.attr({ stroke: this.features.foregroundColor! }); + else path.attr({ fill: this.features.foregroundColor! }); }); } return xml.toString(); @@ -143,49 +159,6 @@ export const HOLI_COLORS = [ "#fa9705", ]; -export const makeFestive = (icon: Icon, clock: Clock = SystemClock): Icon => { - const wrap = ( - icon: Icon, - rule: (clock: Clock) => boolean, - colors: Pick - ) => - new ColorOverridingIcon( - icon, - () => rule(clock), - () => colors - ); - - let result = icon; - - const apply = ( - rule: (clock: Clock) => boolean, - colors: Pick - ) => (result = wrap(result, rule, colors)); - - apply(isChristmas, { - backgroundColor: "green", - foregroundColor: "red", - }); - - const randomHoliColors = _.shuffle([...HOLI_COLORS]); - apply(isHoli, { - backgroundColor: randomHoliColors.pop(), - foregroundColor: randomHoliColors.pop(), - }); - - apply(isCNY, { - backgroundColor: "red", - foregroundColor: "yellow", - }); - - apply(isHalloween, { - backgroundColor: "orange", - foregroundColor: "black", - }); - - return result; -}; - export type ICON = | "artists" | "albums" @@ -237,7 +210,14 @@ export type ICON = | "celtic" | "children" | "chillout" - | "progressiveRock"; + | "progressiveRock" + | "christmas" + | "halloween" + | "yoDragon" + | "yoRabbit" + | "yoTiger" + | "chapel" + | "audioWave"; const iconFrom = (name: string) => new SvgIcon( @@ -246,7 +226,7 @@ const iconFrom = (name: string) => .toString() ); -export const ICONS: Record = { +export const ICONS: Record = { artists: iconFrom("navidrome-artists.svg"), albums: iconFrom("navidrome-all.svg"), blank: iconFrom("blank.svg"), @@ -298,6 +278,13 @@ export const ICONS: Record = { celtic: iconFrom("Scottish-Thistle-108212.svg"), children: iconFrom("Children-78186.svg"), chillout: iconFrom("Sleeping-in Bed-14385.svg"), + christmas: iconFrom("Christmas-Tree-66793.svg"), + halloween: iconFrom("Jack-o' Lantern-66580.svg"), + yoDragon: iconFrom("Year-of Dragon-4537.svg"), + yoRabbit: iconFrom("Year-of Rabbit-6313.svg"), + yoTiger: iconFrom("Year-of Tiger-22776.svg"), + chapel: iconFrom("Chapel-69791.svg"), + audioWave: iconFrom("Audio-Wave-1892.svg"), }; export type RULE = (genre: string) => boolean; @@ -332,6 +319,8 @@ const GENRE_RULES: [RULE, ICON][] = [ [eq("Turntablism"), "vinyl"], [eq("Celtic"), "celtic"], [eq("Progressive Rock"), "progressiveRock"], + [containsWord("Christmas"), "christmas"], + [containsWord("Kerst"), "christmas"], // christmas in dutch [containsWord("Country"), "country"], [containsWord("Rock"), "rock"], [containsWord("Folk"), "guitar"], @@ -347,6 +336,7 @@ const GENRE_RULES: [RULE, ICON][] = [ [eq("Classic"), "classical"], [containsWord("Classical"), "classical"], [containsWord("Comedy"), "comedy"], + [containsWord("Komedie"), "comedy"], // dutch for Comedy [containsWord("Turntable"), "vinyl"], [containsWord("Dub"), "electronic"], [eq("Dubstep"), "electronic"], @@ -374,6 +364,9 @@ const GENRE_RULES: [RULE, ICON][] = [ [contains("Children"), "children"], [contains("Chill"), "chill"], [contains("Old"), "old"], + [containsWord("Christian"), "chapel"], + [containsWord("Religious"), "chapel"], + [containsWord("Spoken"), "audioWave"], ]; export function iconForGenre(genre: string): ICON { @@ -383,3 +376,68 @@ export function iconForGenre(genre: string): ICON { ]; return name! as ICON; } + +export const festivals = (clock: Clock = SystemClock): Transformer => { + const randomHoliColors = _.shuffle([...HOLI_COLORS]); + return allOf( + maybeTransform( + () => isChristmas(clock), + transform({ + svg: ICONS.christmas.svg, + features: { + backgroundColor: "green", + foregroundColor: "red", + }, + }) + ), + maybeTransform( + () => isHoli(clock), + transform({ + features: { + backgroundColor: randomHoliColors.pop(), + foregroundColor: randomHoliColors.pop(), + }, + }) + ), + maybeTransform( + () => isCNY_2022(clock), + transform({ + svg: ICONS.yoTiger.svg, + features: { + backgroundColor: "red", + foregroundColor: "yellow", + }, + }) + ), + maybeTransform( + () => isCNY_2023(clock), + transform({ + svg: ICONS.yoRabbit.svg, + features: { + backgroundColor: "red", + foregroundColor: "yellow", + }, + }) + ), + maybeTransform( + () => isCNY_2024(clock), + transform({ + svg: ICONS.yoDragon.svg, + features: { + backgroundColor: "red", + foregroundColor: "yellow", + }, + }) + ), + maybeTransform( + () => isHalloween(clock), + transform({ + svg: ICONS.halloween.svg, + features: { + backgroundColor: "black", + foregroundColor: "orange", + }, + }) + ) + ); +}; diff --git a/src/server.ts b/src/server.ts index c3c3bb6..50c57c2 100644 --- a/src/server.ts +++ b/src/server.ts @@ -25,7 +25,7 @@ import { Clock, SystemClock } from "./clock"; import { pipe } from "fp-ts/lib/function"; import { URLBuilder } from "./url_builder"; import makeI8N, { asLANGs, KEY, keys as i8nKeys, LANG } from "./i8n"; -import { Icon, makeFestive, ICONS } from "./icon"; +import { Icon, ICONS, festivals, features } from "./icon"; import _, { shuffle } from "underscore"; import morgan from "morgan"; import { takeWithRepeats } from "./utils"; @@ -79,7 +79,7 @@ export type ServerOpts = { }; applyContextPath: boolean; logRequests: boolean; - version: string + version: string; }; const DEFAULT_SERVER_OPTS: ServerOpts = { @@ -89,7 +89,7 @@ const DEFAULT_SERVER_OPTS: ServerOpts = { iconColors: { foregroundColor: undefined, backgroundColor: undefined }, applyContextPath: true, logRequests: false, - version: "v?" + version: "v?", }; function server( @@ -148,7 +148,7 @@ function server( removeRegistrationRoute: bonobUrl .append({ pathname: REMOVE_REGISTRATION_ROUTE }) .pathname(), - version: opts.version + version: opts.version, }); } ); @@ -413,10 +413,15 @@ function server( }; return Promise.resolve( - makeFestive( - icon.with({ viewPortIncreasePercent: 80, ...serverOpts.iconColors }), - clock - ).toString() + icon + .apply( + features({ + viewPortIncreasePercent: 80, + ...serverOpts.iconColors, + }) + ) + .apply(festivals(clock)) + .toString() ) .then(spec.responseFormatter) .then((data) => res.status(200).type(spec.mimeType).send(data)); @@ -428,7 +433,12 @@ function server( icons: Object.keys(ICONS).map((k) => [ k, ((ICONS as any)[k] as Icon) - .with({ viewPortIncreasePercent: 80, ...serverOpts.iconColors }) + .apply( + features({ + viewPortIncreasePercent: 80, + ...serverOpts.iconColors, + }) + ) .toString() .replace('', ""), ]), diff --git a/tests/icon.test.ts b/tests/icon.test.ts index b41ba7d..381d007 100644 --- a/tests/icon.test.ts +++ b/tests/icon.test.ts @@ -2,16 +2,22 @@ import dayjs from "dayjs"; import libxmljs from "libxmljs2"; import { - ColorOverridingIcon, contains, containsWord, eq, HOLI_COLORS, Icon, iconForGenre, - makeFestive, SvgIcon, - Transformation, + IconFeatures, + IconSpec, + ICONS, + Transformer, + transform, + maybeTransform, + festivals, + allOf, + features, } from "../src/icon"; describe("SvgIcon", () => { @@ -34,7 +40,7 @@ describe("SvgIcon", () => { `; - describe("with no transformation", () => { + describe("with no features", () => { it("should be the same", () => { expect(new SvgIcon(svgIcon24).toString()).toEqual(xmlTidy(svgIcon24)); }); @@ -46,7 +52,7 @@ describe("SvgIcon", () => { it("should resize the viewPort", () => { expect( new SvgIcon(svgIcon24) - .with({ viewPortIncreasePercent: 50 }) + .with({ features: { viewPortIncreasePercent: 50 } }) .toString() ).toEqual( xmlTidy(` @@ -63,7 +69,7 @@ describe("SvgIcon", () => { it("should resize the viewPort", () => { expect( new SvgIcon(svgIcon128) - .with({ viewPortIncreasePercent: 50 }) + .with({ features: { viewPortIncreasePercent: 50 } }) .toString() ).toEqual( xmlTidy(` @@ -81,7 +87,9 @@ describe("SvgIcon", () => { describe("of 0%", () => { it("should do nothing", () => { expect( - new SvgIcon(svgIcon24).with({ viewPortIncreasePercent: 0 }).toString() + new SvgIcon(svgIcon24) + .with({ features: { viewPortIncreasePercent: 0 } }) + .toString() ).toEqual(xmlTidy(svgIcon24)); }); }); @@ -91,7 +99,9 @@ describe("SvgIcon", () => { describe("with no viewPort increase", () => { it("should add a rectangle the same size as the original viewPort", () => { expect( - new SvgIcon(svgIcon24).with({ backgroundColor: "red" }).toString() + new SvgIcon(svgIcon24) + .with({ features: { backgroundColor: "red" } }) + .toString() ).toEqual( xmlTidy(` @@ -109,7 +119,12 @@ describe("SvgIcon", () => { it("should add a rectangle the same size as the original viewPort", () => { expect( new SvgIcon(svgIcon24) - .with({ backgroundColor: "pink", viewPortIncreasePercent: 50 }) + .with({ + features: { + backgroundColor: "pink", + viewPortIncreasePercent: 50, + }, + }) .toString() ).toEqual( xmlTidy(` @@ -127,7 +142,9 @@ describe("SvgIcon", () => { describe("of undefined", () => { it("should not do anything", () => { expect( - new SvgIcon(svgIcon24).with({ backgroundColor: undefined }).toString() + new SvgIcon(svgIcon24) + .with({ features: { backgroundColor: undefined } }) + .toString() ).toEqual( xmlTidy(` @@ -144,8 +161,8 @@ describe("SvgIcon", () => { it("should use the most recent", () => { expect( new SvgIcon(svgIcon24) - .with({ backgroundColor: "green" }) - .with({ backgroundColor: "red" }) + .with({ features: { backgroundColor: "green" } }) + .with({ features: { backgroundColor: "red" } }) .toString() ).toEqual( xmlTidy(` @@ -165,7 +182,9 @@ describe("SvgIcon", () => { describe("with no viewPort increase", () => { it("should add a rectangle the same size as the original viewPort", () => { expect( - new SvgIcon(svgIcon24).with({ foregroundColor: "red" }).toString() + new SvgIcon(svgIcon24) + .with({ features: { foregroundColor: "red" } }) + .toString() ).toEqual( xmlTidy(` @@ -182,7 +201,12 @@ describe("SvgIcon", () => { it("should add a rectangle the same size as the original viewPort", () => { expect( new SvgIcon(svgIcon24) - .with({ foregroundColor: "pink", viewPortIncreasePercent: 50 }) + .with({ + features: { + foregroundColor: "pink", + viewPortIncreasePercent: 50, + }, + }) .toString() ).toEqual( xmlTidy(` @@ -199,7 +223,9 @@ describe("SvgIcon", () => { describe("of undefined", () => { it("should not do anything", () => { expect( - new SvgIcon(svgIcon24).with({ foregroundColor: undefined }).toString() + new SvgIcon(svgIcon24) + .with({ features: { foregroundColor: undefined } }) + .toString() ).toEqual( xmlTidy(` @@ -216,8 +242,8 @@ describe("SvgIcon", () => { it("should use the most recent", () => { expect( new SvgIcon(svgIcon24) - .with({ foregroundColor: "blue" }) - .with({ foregroundColor: "red" }) + .with({ features: { foregroundColor: "blue" } }) + .with({ features: { foregroundColor: "red" } }) .toString() ).toEqual( xmlTidy(` @@ -231,143 +257,306 @@ describe("SvgIcon", () => { }); }); }); + + describe("swapping the svg", () => { + describe("with no other changes", () => { + it("should swap out the svg, but maintain the IconFeatures", () => { + expect( + new SvgIcon(svgIcon24, { + foregroundColor: "blue", + backgroundColor: "green", + viewPortIncreasePercent: 50, + }) + .with({ svg: svgIcon128 }) + .toString() + ).toEqual( + xmlTidy(` + + + + + + + `) + ); + }); + }); + + describe("with no other changes", () => { + it("should swap out the svg, but maintain the IconFeatures", () => { + expect( + new SvgIcon(svgIcon24, { + foregroundColor: "blue", + backgroundColor: "green", + viewPortIncreasePercent: 50, + }) + .with({ + svg: svgIcon128, + features: { + foregroundColor: "pink", + backgroundColor: "red", + viewPortIncreasePercent: 0, + }, + }) + .toString() + ).toEqual( + xmlTidy(` + + + + + + + `) + ); + }); + }); + }); }); class DummyIcon implements Icon { - transformation: Partial; - constructor(transformation: Partial) { - this.transformation = transformation; + svg: string; + features: Partial; + constructor(svg: string, features: Partial) { + this.svg = svg; + this.features = features; } - public with = (newTransformation: Partial) => - new DummyIcon({ ...this.transformation, ...newTransformation }); - public toString = () => JSON.stringify(this); + public apply = (transformer: Transformer): Icon => transformer(this); + + public with = ({ svg, features }: Partial) => { + return new DummyIcon(svg || this.svg, { + ...this.features, + ...(features || {}), + }); + }; + + public toString = () => + JSON.stringify({ svg: this.svg, features: this.features }); } -describe("ColorOverridingIcon", () => { +describe("transform", () => { + describe("when the features contains no svg", () => { + it("should apply the overriding transform ontop of the requested transform", () => { + const original = new DummyIcon("original", { + backgroundColor: "black", + foregroundColor: "black", + }); + const result = original + .with({ + features: { + viewPortIncreasePercent: 100, + foregroundColor: "blue", + backgroundColor: "blue", + }, + }) + .apply( + transform({ + features: { + foregroundColor: "override1", + backgroundColor: "override2", + }, + }) + ) as DummyIcon; + + expect(result.svg).toEqual("original"); + expect(result.features).toEqual({ + viewPortIncreasePercent: 100, + foregroundColor: "override1", + backgroundColor: "override2", + }); + }); + }); + + describe("when the features contains an svg", () => { + it("should use the newly provided svg", () => { + const original = new DummyIcon("original", { + backgroundColor: "black", + foregroundColor: "black", + }); + const result = original + .with({ + features: { + viewPortIncreasePercent: 100, + foregroundColor: "blue", + backgroundColor: "blue", + }, + }) + .apply( + transform({ + svg: "new", + }) + ) as DummyIcon; + + expect(result.svg).toEqual("new"); + expect(result.features).toEqual({ + viewPortIncreasePercent: 100, + foregroundColor: "blue", + backgroundColor: "blue", + }); + }); + }); +}); + +describe("features", () => { + it("should apply the features", () => { + const original = new DummyIcon("original", { + backgroundColor: "black", + foregroundColor: "black", + }); + const result = original.apply( + features({ + viewPortIncreasePercent: 100, + foregroundColor: "blue", + backgroundColor: "blue", + }) + ) as DummyIcon; + + expect(result.features).toEqual({ + viewPortIncreasePercent: 100, + foregroundColor: "blue", + backgroundColor: "blue", + }); + }); +}); + +describe("allOf", () => { + it("should apply all composed transforms", () => { + const result = new DummyIcon("original", { + foregroundColor: "black", + backgroundColor: "black", + viewPortIncreasePercent: 0, + }).apply( + allOf( + (icon: Icon) => icon.with({ svg: "foo" }), + (icon: Icon) => icon.with({ features: { backgroundColor: "red" } }), + (icon: Icon) => icon.with({ features: { foregroundColor: "blue" } }) + ) + ) as DummyIcon; + + expect(result.svg).toEqual("foo"); + expect(result.features).toEqual({ + foregroundColor: "blue", + backgroundColor: "red", + viewPortIncreasePercent: 0, + }); + }); +}); + +describe("maybeTransform", () => { describe("when the rule matches", () => { - const icon = new DummyIcon({ + const original = new DummyIcon("original", { backgroundColor: "black", foregroundColor: "black", }); - describe("overriding some options", () => { - const overriding = new ColorOverridingIcon( - icon, - () => true, - () => ({ backgroundColor: "blue", foregroundColor: "red" }) - ); - - describe("with", () => { - it("should be the with of the underlieing icon with the overriden colors", () => { - const result = overriding.with({ + describe("transforming the color", () => { + const result = original + .with({ + features: { viewPortIncreasePercent: 99, backgroundColor: "shouldBeIgnored", foregroundColor: "shouldBeIgnored", - }) as DummyIcon; + }, + }) + .apply( + maybeTransform( + () => true, + transform({ + features: { + backgroundColor: "blue", + foregroundColor: "red", + }, + }) + ) + ) as DummyIcon; - expect(result.transformation).toEqual({ + describe("with", () => { + it("should be the with of the underlieing icon with the overriden colors", () => { + expect(result.svg).toEqual("original"); + expect(result.features).toEqual({ viewPortIncreasePercent: 99, backgroundColor: "blue", foregroundColor: "red", }); }); }); - - describe("toString", () => { - it("should be the toString of the underlieing icon with the overriden colors", () => { - expect(overriding.toString()).toEqual( - new DummyIcon({ - backgroundColor: "blue", - foregroundColor: "red", - }).toString() - ); - }); - }); }); describe("overriding all options", () => { - const overriding = new ColorOverridingIcon( - icon, - () => true, - () => ({ - backgroundColor: "blue", - foregroundColor: "red", - }) - ); - - describe("with", () => { - it("should be the with of the underlieing icon with the overriden colors", () => { - const result = overriding.with({ + const result = original + .with({ + features: { viewPortIncreasePercent: 99, backgroundColor: "shouldBeIgnored", foregroundColor: "shouldBeIgnored", - }) as DummyIcon; + }, + }) + .apply( + maybeTransform( + () => true, + transform({ + features: { + backgroundColor: "blue", + foregroundColor: "red", + }, + }) + ) + ) as DummyIcon; - expect(result.transformation).toEqual({ + describe("with", () => { + it("should be the with of the underlieing icon with the overriden colors", () => { + expect(result.features).toEqual({ viewPortIncreasePercent: 99, backgroundColor: "blue", foregroundColor: "red", }); }); }); - - describe("toString", () => { - it("should be the toString of the underlieing icon with the overriden colors", () => { - expect(overriding.toString()).toEqual( - new DummyIcon({ - backgroundColor: "blue", - foregroundColor: "red", - }).toString() - ); - }); - }); }); }); describe("when the rule doesnt match", () => { - const icon = new DummyIcon({ + const original = new DummyIcon("original", { backgroundColor: "black", foregroundColor: "black", }); - const overriding = new ColorOverridingIcon( - icon, - () => false, - () => ({ backgroundColor: "blue", foregroundColor: "red" }) - ); - - describe("with", () => { - it("should use the provided transformation", () => { - const result = overriding.with({ + const result = original + .with({ + features: { viewPortIncreasePercent: 88, backgroundColor: "shouldBeUsed", foregroundColor: "shouldBeUsed", - }) as DummyIcon; + }, + }) + .apply( + maybeTransform( + () => false, + transform({ + features: { backgroundColor: "blue", foregroundColor: "red" }, + }) + ) + ) as DummyIcon; - expect(result.transformation).toEqual({ + describe("with", () => { + it("should use the provided features", () => { + expect(result.features).toEqual({ viewPortIncreasePercent: 88, backgroundColor: "shouldBeUsed", foregroundColor: "shouldBeUsed", }); }); }); - - describe("toString", () => { - it("should be the toString of the unchanged icon", () => { - expect(overriding.toString()).toEqual(icon.toString()); - }); - }); }); }); -describe("makeFestive", () => { - const icon = new DummyIcon({ +describe("festivals", () => { + const original = new DummyIcon("original", { backgroundColor: "black", foregroundColor: "black", }); let now = dayjs(); - - const festiveIcon = makeFestive(icon, { now: () => now }); + const clock = { now: () => now }; describe("on a day that isn't festive", () => { beforeEach(() => { @@ -375,17 +564,23 @@ describe("makeFestive", () => { }); it("should use the given colors", () => { - const result = festiveIcon.with({ - viewPortIncreasePercent: 88, - backgroundColor: "shouldBeUsed", - foregroundColor: "shouldBeUsed", - }) as DummyIcon; + const result = original + .apply( + features({ + viewPortIncreasePercent: 88, + backgroundColor: "shouldBeUsed", + foregroundColor: "shouldBeUsed", + }) + ) + .apply(festivals(clock)) as DummyIcon; - expect(result.transformation).toEqual({ - viewPortIncreasePercent: 88, - backgroundColor: "shouldBeUsed", - foregroundColor: "shouldBeUsed", - }); + expect(result.toString()).toEqual( + new DummyIcon("original", { + backgroundColor: "shouldBeUsed", + foregroundColor: "shouldBeUsed", + viewPortIncreasePercent: 88, + }).toString() + ); }); }); @@ -395,16 +590,22 @@ describe("makeFestive", () => { }); it("should use the christmas theme colors", () => { - const result = festiveIcon.with({ - viewPortIncreasePercent: 25, - backgroundColor: "shouldNotBeUsed", - foregroundColor: "shouldNotBeUsed", - }) as DummyIcon; + const result = original.apply( + allOf( + features({ + viewPortIncreasePercent: 25, + backgroundColor: "shouldNotBeUsed", + foregroundColor: "shouldNotBeUsed", + }), + festivals(clock) + ) + ) as DummyIcon; - expect(result.transformation).toEqual({ - viewPortIncreasePercent: 25, + expect(result.svg).toEqual(ICONS.christmas.svg); + expect(result.features).toEqual({ backgroundColor: "green", foregroundColor: "red", + viewPortIncreasePercent: 25, }); }); }); @@ -415,36 +616,98 @@ describe("makeFestive", () => { }); it("should use the given colors", () => { - const result = festiveIcon.with({ - viewPortIncreasePercent: 12, - backgroundColor: "shouldNotBeUsed", - foregroundColor: "shouldNotBeUsed", - }) as DummyIcon; + const result = original + .apply( + features({ + viewPortIncreasePercent: 12, + backgroundColor: "shouldNotBeUsed", + foregroundColor: "shouldNotBeUsed", + }) + ) + .apply(festivals(clock)) as DummyIcon; - expect(result.transformation).toEqual({ + expect(result.svg).toEqual(ICONS.halloween.svg); + expect(result.features).toEqual({ viewPortIncreasePercent: 12, - backgroundColor: "orange", - foregroundColor: "black", + backgroundColor: "black", + foregroundColor: "orange", }); }); }); describe("on cny", () => { - beforeEach(() => { - now = dayjs("2022/02/01"); + describe("2022", () => { + beforeEach(() => { + now = dayjs("2022/02/01"); + }); + + it("should use the cny theme", () => { + const result = original + .apply( + features({ + viewPortIncreasePercent: 12, + backgroundColor: "shouldNotBeUsed", + foregroundColor: "shouldNotBeUsed", + }) + ) + .apply(festivals(clock)) as DummyIcon; + + expect(result.svg).toEqual(ICONS.yoTiger.svg); + expect(result.features).toEqual({ + viewPortIncreasePercent: 12, + backgroundColor: "red", + foregroundColor: "yellow", + }); + }); }); - it("should use the given colors", () => { - const result = festiveIcon.with({ - viewPortIncreasePercent: 12, - backgroundColor: "shouldNotBeUsed", - foregroundColor: "shouldNotBeUsed", - }) as DummyIcon; + describe("2023", () => { + beforeEach(() => { + now = dayjs("2023/01/22"); + }); - expect(result.transformation).toEqual({ - viewPortIncreasePercent: 12, - backgroundColor: "red", - foregroundColor: "yellow", + it("should use the cny theme", () => { + const result = original + .apply( + features({ + viewPortIncreasePercent: 12, + backgroundColor: "shouldNotBeUsed", + foregroundColor: "shouldNotBeUsed", + }) + ) + .apply(festivals(clock)) as DummyIcon; + + expect(result.svg).toEqual(ICONS.yoRabbit.svg); + expect(result.features).toEqual({ + viewPortIncreasePercent: 12, + backgroundColor: "red", + foregroundColor: "yellow", + }); + }); + }); + + describe("2024", () => { + beforeEach(() => { + now = dayjs("2024/02/10"); + }); + + it("should use the cny theme", () => { + const result = original + .apply( + features({ + viewPortIncreasePercent: 12, + backgroundColor: "shouldNotBeUsed", + foregroundColor: "shouldNotBeUsed", + }) + ) + .apply(festivals(clock)) as DummyIcon; + + expect(result.svg).toEqual(ICONS.yoDragon.svg); + expect(result.features).toEqual({ + viewPortIncreasePercent: 12, + backgroundColor: "red", + foregroundColor: "yellow", + }); }); }); }); @@ -455,21 +718,25 @@ describe("makeFestive", () => { }); it("should use the given colors", () => { - const result = festiveIcon.with({ - viewPortIncreasePercent: 12, - backgroundColor: "shouldNotBeUsed", - foregroundColor: "shouldNotBeUsed", - }) as DummyIcon; + const result = original + .apply( + features({ + viewPortIncreasePercent: 12, + backgroundColor: "shouldNotBeUsed", + foregroundColor: "shouldNotBeUsed", + }) + ) + .apply(festivals(clock)) as DummyIcon; - expect(result.transformation.viewPortIncreasePercent).toEqual(12); - expect( - HOLI_COLORS.includes(result.transformation.backgroundColor!) - ).toEqual(true); - expect( - HOLI_COLORS.includes(result.transformation.foregroundColor!) - ).toEqual(true); - expect(result.transformation.backgroundColor).not.toEqual( - result.transformation.foregroundColor + expect(result.features.viewPortIncreasePercent).toEqual(12); + expect(HOLI_COLORS.includes(result.features.backgroundColor!)).toEqual( + true + ); + expect(HOLI_COLORS.includes(result.features.foregroundColor!)).toEqual( + true + ); + expect(result.features.backgroundColor).not.toEqual( + result.features.foregroundColor ); }); }); @@ -499,7 +766,7 @@ describe("containsWord", () => { it("should be true word is a substring with space delim", () => { expect(containsWord("Foo")("some foo bar")).toEqual(true); }); - + it("should be true word is a substring with hyphen delim", () => { expect(containsWord("Foo")("some----foo-bar")).toEqual(true); }); @@ -509,7 +776,6 @@ describe("containsWord", () => { }); }); - describe("iconForGenre", () => { [ ["Acid House", "mushroom"], diff --git a/tests/server.test.ts b/tests/server.test.ts index 72fb4ac..b0ba8cc 100644 --- a/tests/server.test.ts +++ b/tests/server.test.ts @@ -1723,16 +1723,28 @@ describe("server", () => { expect(svg).toContain(`fill="brightpink"`); }); - it("should return a christmas icon on christmas day", async () => { - const response = await request( - server({ now: () => dayjs("2022/12/25") }) - ).get(`/icon/${type}/size/180`); + function itShouldBeFestive(theme: string, date: string, id: string, color1: string, color2: string) { + it(`should return a ${theme} icon on ${date}`, async () => { + const response = await request( + server({ now: () => dayjs(date) }) + ).get(`/icon/${type}/size/180`); + + expect(response.status).toEqual(200); + const svg = Buffer.from(response.body).toString(); + expect(svg).toContain(`id="${id}"`); + expect(svg).toContain(`fill="${color1}"`); + expect(svg).toContain(`fill="${color2}"`); + }); + } - expect(response.status).toEqual(200); - const svg = Buffer.from(response.body).toString(); - expect(svg).toContain(`fill="red"`); - expect(svg).toContain(`fill="green"`); - }); + itShouldBeFestive("christmas '22", "2022/12/25", "christmas", "red", "green") + itShouldBeFestive("christmas '23", "2023/12/25", "christmas", "red", "green") + + itShouldBeFestive("halloween", "2022/10/31", "halloween", "black", "orange") + itShouldBeFestive("halloween", "2023/10/31", "halloween", "black", "orange") + + itShouldBeFestive("cny '22", "2022/02/01", "yoTiger", "red", "yellow") + itShouldBeFestive("cny '23", "2023/01/22", "yoRabbit", "red", "yellow") }); }); }); diff --git a/web/icons/Audio-Wave-1892.svg b/web/icons/Audio-Wave-1892.svg new file mode 100644 index 0000000..2b9da57 --- /dev/null +++ b/web/icons/Audio-Wave-1892.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/web/icons/Chapel-69791.svg b/web/icons/Chapel-69791.svg new file mode 100644 index 0000000..399b7c3 --- /dev/null +++ b/web/icons/Chapel-69791.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/web/icons/Christmas-Tree-66793.svg b/web/icons/Christmas-Tree-66793.svg new file mode 100644 index 0000000..1e11708 --- /dev/null +++ b/web/icons/Christmas-Tree-66793.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/web/icons/Jack-o' Lantern-107512.svg b/web/icons/Jack-o' Lantern-107512.svg new file mode 100644 index 0000000..86ff199 --- /dev/null +++ b/web/icons/Jack-o' Lantern-107512.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/web/icons/Jack-o' Lantern-66580.svg b/web/icons/Jack-o' Lantern-66580.svg new file mode 100644 index 0000000..9096795 --- /dev/null +++ b/web/icons/Jack-o' Lantern-66580.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/web/icons/Year-of Dragon-4537.svg b/web/icons/Year-of Dragon-4537.svg new file mode 100644 index 0000000..7be530a --- /dev/null +++ b/web/icons/Year-of Dragon-4537.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/web/icons/Year-of Rabbit-6313.svg b/web/icons/Year-of Rabbit-6313.svg new file mode 100644 index 0000000..0aa1b8c --- /dev/null +++ b/web/icons/Year-of Rabbit-6313.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/web/icons/Year-of Tiger-22776.svg b/web/icons/Year-of Tiger-22776.svg new file mode 100644 index 0000000..f2527f3 --- /dev/null +++ b/web/icons/Year-of Tiger-22776.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file