import dayjs from "dayjs"; import { FixedClock } from "../src/clock"; import { xmlTidy } from "../src/utils"; import { contains, containsWord, eq, HOLI_COLORS, Icon, iconForGenre, SvgIcon, IconFeatures, IconSpec, ICONS, Transformer, transform, maybeTransform, festivals, allOf, features, STAR_WARS, NO_FEATURES, } from "../src/icon"; describe("SvgIcon", () => { const svgIcon24 = ` 80's 80's `; const svgIcon128 = ` `; describe("with no features", () => { it("should be the same", () => { expect(new SvgIcon(svgIcon24).toString()).toEqual(xmlTidy(svgIcon24)); }); }); describe("with a view port increase", () => { describe("of 50%", () => { describe("when the viewPort is of size 0 0 24 24", () => { it("should resize the viewPort", () => { expect( new SvgIcon(svgIcon24) .with({ features: { viewPortIncreasePercent: 50 } }) .toString() ).toEqual( xmlTidy(` 80's 80's `) ); }); }); describe("when the viewPort is of size 0 0 128 128", () => { it("should resize the viewPort", () => { expect( new SvgIcon(svgIcon128) .with({ features: { viewPortIncreasePercent: 50 } }) .toString() ).toEqual( xmlTidy(` `) ); }); }); }); describe("of 0%", () => { it("should do nothing", () => { expect( new SvgIcon(svgIcon24) .with({ features: { viewPortIncreasePercent: 0 } }) .toString() ).toEqual(xmlTidy(svgIcon24)); }); }); }); describe("background color", () => { describe("with no viewPort increase", () => { it("should add a rectangle the same size as the original viewPort", () => { expect( new SvgIcon(svgIcon24) .with({ features: { backgroundColor: "red" } }) .toString() ).toEqual( xmlTidy(` 80's 80's `) ); }); }); describe("with a viewPort increase", () => { it("should add a rectangle the same size as the original viewPort", () => { expect( new SvgIcon(svgIcon24) .with({ features: { backgroundColor: "pink", viewPortIncreasePercent: 50, }, }) .toString() ).toEqual( xmlTidy(` 80's 80's `) ); }); }); describe("of undefined", () => { it("should not do anything", () => { expect( new SvgIcon(svgIcon24) .with({ features: { backgroundColor: undefined } }) .toString() ).toEqual( xmlTidy(` 80's 80's `) ); }); }); describe("multiple times", () => { it("should use the most recent", () => { expect( new SvgIcon(svgIcon24) .with({ features: { backgroundColor: "green" } }) .with({ features: { backgroundColor: "red" } }) .toString() ).toEqual( xmlTidy(` 80's 80's `) ); }); }); }); describe("foreground color", () => { describe("with no viewPort increase", () => { it("should change the fill values", () => { expect( new SvgIcon(svgIcon24) .with({ features: { foregroundColor: "red" } }) .toString() ).toEqual( xmlTidy(` 80's 80's `) ); }); }); describe("with a viewPort increase", () => { it("should change the fill values", () => { expect( new SvgIcon(svgIcon24) .with({ features: { foregroundColor: "pink", viewPortIncreasePercent: 50, }, }) .toString() ).toEqual( xmlTidy(` 80's 80's `) ); }); }); describe("of undefined", () => { it("should not do anything", () => { expect( new SvgIcon(svgIcon24) .with({ features: { foregroundColor: undefined } }) .toString() ).toEqual( xmlTidy(` 80's 80's `) ); }); }); describe("mutliple times", () => { it("should use the most recent", () => { expect( new SvgIcon(svgIcon24) .with({ features: { foregroundColor: "blue" } }) .with({ features: { foregroundColor: "red" } }) .toString() ).toEqual( xmlTidy(` 80's 80's `) ); }); }); }); describe("text", () => { describe("when text value specified", () => { it("should change the text values", () => { expect( new SvgIcon(svgIcon24) .with({ features: { text: "yipppeeee" } }) .toString() ).toEqual( xmlTidy(` yipppeeee yipppeeee `) ); }); }); describe("of undefined", () => { it("should not do anything", () => { expect( new SvgIcon(svgIcon24) .with({ features: { text: undefined } }) .toString() ).toEqual( xmlTidy(` 80's 80's `) ); }); }); }); 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 { svg: string; features: IconFeatures; constructor(svg: string, features: Partial) { this.svg = svg; this.features = { ...NO_FEATURES, ...features }; } 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("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", text: "a", }, }) .apply( transform({ features: { foregroundColor: "override1", backgroundColor: "override2", text: "b", }, }) ) as DummyIcon; expect(result.svg).toEqual("original"); expect(result.features).toEqual({ viewPortIncreasePercent: 100, foregroundColor: "override1", backgroundColor: "override2", text: "b", }); }); }); 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", text: "bob", }, }) .apply( transform({ svg: "new", }) ) as DummyIcon; expect(result.svg).toEqual("new"); expect(result.features).toEqual({ viewPortIncreasePercent: 100, foregroundColor: "blue", backgroundColor: "blue", text: "bob" }); }); }); }); 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", text: "foobar" }) ) as DummyIcon; expect(result.features).toEqual({ viewPortIncreasePercent: 100, foregroundColor: "blue", backgroundColor: "blue", text: "foobar" }); }); }); 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 original = new DummyIcon("original", { backgroundColor: "black", foregroundColor: "black", }); describe("transforming the color", () => { const result = original .with({ features: { viewPortIncreasePercent: 99, backgroundColor: "shouldBeIgnored", foregroundColor: "shouldBeIgnored", }, }) .apply( maybeTransform( () => true, transform({ features: { backgroundColor: "blue", foregroundColor: "red", }, }) ) ) as DummyIcon; 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("overriding all options", () => { const result = original .with({ features: { viewPortIncreasePercent: 99, backgroundColor: "shouldBeIgnored", foregroundColor: "shouldBeIgnored", }, }) .apply( maybeTransform( () => true, transform({ features: { backgroundColor: "blue", foregroundColor: "red", }, }) ) ) as DummyIcon; 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("when the rule doesnt match", () => { const original = new DummyIcon("original", { backgroundColor: "black", foregroundColor: "black", }); const result = original .with({ features: { viewPortIncreasePercent: 88, backgroundColor: "shouldBeUsed", foregroundColor: "shouldBeUsed", }, }) .apply( maybeTransform( () => false, transform({ features: { backgroundColor: "blue", foregroundColor: "red" }, }) ) ) as DummyIcon; describe("with", () => { it("should use the provided features", () => { expect(result.features).toEqual({ viewPortIncreasePercent: 88, backgroundColor: "shouldBeUsed", foregroundColor: "shouldBeUsed", }); }); }); }); }); describe("festivals", () => { const original = new DummyIcon("original", { backgroundColor: "black", foregroundColor: "black", }); const clock = new FixedClock(dayjs()); describe("on a day that isn't festive", () => { beforeEach(() => { clock.time = dayjs("2022/10/12"); }); it("should use the given colors", () => { const result = original .apply( features({ viewPortIncreasePercent: 88, backgroundColor: "shouldBeUsed", foregroundColor: "shouldBeUsed", }) ) .apply(festivals(clock)) as DummyIcon; expect(result.toString()).toEqual( new DummyIcon("original", { backgroundColor: "shouldBeUsed", foregroundColor: "shouldBeUsed", viewPortIncreasePercent: 88, }).toString() ); }); }); describe("on christmas day", () => { beforeEach(() => { clock.time = dayjs("2022/12/25"); }); it("should use the christmas theme colors", () => { const result = original.apply( allOf( features({ viewPortIncreasePercent: 25, backgroundColor: "shouldNotBeUsed", foregroundColor: "shouldNotBeUsed", }), festivals(clock) ) ) as DummyIcon; expect(result.svg).toEqual(ICONS.christmas.svg); expect(result.features).toEqual({ backgroundColor: "green", foregroundColor: "red", viewPortIncreasePercent: 25, }); }); }); describe("on halloween", () => { beforeEach(() => { clock.time = dayjs("2022/10/31"); }); it("should use the given colors", () => { const result = original .apply( features({ viewPortIncreasePercent: 12, backgroundColor: "shouldNotBeUsed", foregroundColor: "shouldNotBeUsed", }) ) .apply(festivals(clock)) as DummyIcon; expect(result.svg).toEqual(ICONS.halloween.svg); expect(result.features).toEqual({ viewPortIncreasePercent: 12, backgroundColor: "black", foregroundColor: "orange", }); }); }); describe("on may 4", () => { beforeEach(() => { clock.time = dayjs("2022/5/4"); }); it("should use the undefined colors, so no color", () => { const result = original .apply( features({ viewPortIncreasePercent: 12, backgroundColor: "shouldNotBeUsed", foregroundColor: "shouldNotBeUsed", }) ) .apply(festivals(clock)) as DummyIcon; expect(STAR_WARS.map(it => it.svg)).toContain(result.svg); expect(result.features).toEqual({ viewPortIncreasePercent: 12, backgroundColor: undefined, foregroundColor: undefined, }); }); }); describe("on cny", () => { describe("2022", () => { beforeEach(() => { clock.time = 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", }); }); }); describe("2023", () => { beforeEach(() => { clock.time = dayjs("2023/01/22"); }); 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(() => { clock.time = 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", }); }); }); }); describe("on holi", () => { beforeEach(() => { clock.time = dayjs("2022/03/18"); }); it("should use the given colors", () => { const result = original .apply( features({ viewPortIncreasePercent: 12, backgroundColor: "shouldNotBeUsed", foregroundColor: "shouldNotBeUsed", }) ) .apply(festivals(clock)) as DummyIcon; 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 ); }); }); }); describe("eq", () => { it("should be true when ===", () => { expect(eq("Foo")("foo")).toEqual(true); }); it("should be false when not ===", () => { expect(eq("Foo")("bar")).toEqual(false); }); }); describe("contains", () => { it("should be true word is a substring", () => { expect(contains("Foo")("some foo bar")).toEqual(true); }); it("should be false when not ===", () => { expect(contains("Foo")("some bar")).toEqual(false); }); }); 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); }); it("should be false when not ===", () => { expect(containsWord("Foo")("somefoobar")).toEqual(false); }); }); describe("iconForGenre", () => { [ ["Acid House", "mushroom"], ["African", "african"], ["Alternative Rock", "rock"], ["Americana", "americana"], ["Anti-Folk", "guitar"], ["Audio-Book", "book"], ["Australian Hip Hop", "oz"], ["Rap", "rap"], ["Hip Hop", "hipHop"], ["Hip-Hop", "hipHop"], ["Metal", "metal"], ["Horrorcore", "horror"], ["Punk", "punk"], ["blah", "music"], ].forEach(([genre, expected]) => { describe(`a genre of ${genre}`, () => { it(`should have an icon of ${expected}`, () => { const name = iconForGenre(genre!)!; expect(name).toEqual(expected); }); }); describe(`a genre of ${genre!.toLowerCase()}`, () => { it(`should have an icon of ${expected}`, () => { const name = iconForGenre(genre!)!; expect(name).toEqual(expected); }); }); }); });