mirror of
https://github.com/emilybache/GildedRose-Refactoring-Kata.git
synced 2026-02-10 04:01:19 +00:00
Merge pull request #1 from arnochauveau/refactor-ts-exercise
Refactor ts exercise
This commit is contained in:
commit
1b80c27e67
@ -1,11 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
require: [
|
|
||||||
"ts-node/register",
|
|
||||||
"tsconfig-paths/register",
|
|
||||||
"source-map-support/register"
|
|
||||||
],
|
|
||||||
recursive: true,
|
|
||||||
spec: "test/mocha/*.spec.ts"
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
extension: [
|
|
||||||
".ts"
|
|
||||||
],
|
|
||||||
exclude: [
|
|
||||||
"**/*.d.ts",
|
|
||||||
"test/**",
|
|
||||||
"/*.js"
|
|
||||||
],
|
|
||||||
require: [
|
|
||||||
"ts-node/register"
|
|
||||||
],
|
|
||||||
reporter: [
|
|
||||||
"html",
|
|
||||||
"text"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
3
TypeScript/app/config.ts
Normal file
3
TypeScript/app/config.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const config = {
|
||||||
|
maxQuality: 50,
|
||||||
|
};
|
||||||
38
TypeScript/app/gilded-rose.spec.ts
Normal file
38
TypeScript/app/gilded-rose.spec.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
const getUpdateBehaviorMock = jest.fn((item: Item) => new MockBehavior(item));
|
||||||
|
const updateMock = jest.fn((item: Item) => item);
|
||||||
|
|
||||||
|
jest.mock("@app/update-behaviors", () => ({
|
||||||
|
getUpdateBehaviorFor: getUpdateBehaviorMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { GildedRose } from "@app/gilded-rose";
|
||||||
|
import { Item } from "@app/item";
|
||||||
|
import { IUpdateBehavior } from "@app/update-behaviors";
|
||||||
|
|
||||||
|
export class MockBehavior implements IUpdateBehavior {
|
||||||
|
constructor(public item: Item) {}
|
||||||
|
update() {
|
||||||
|
return updateMock(this.item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Gilded Rose", () => {
|
||||||
|
it("should have an empty array as items when no constructor parameter is provided", () => {
|
||||||
|
const gildedRose = new GildedRose();
|
||||||
|
expect(gildedRose.items).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call the behavior resolver and update function for each item", () => {
|
||||||
|
const item1 = new Item("item 1", 0, 0);
|
||||||
|
const item2 = new Item("item 2", 0, 0);
|
||||||
|
const gildedRose = new GildedRose([item1, item2]);
|
||||||
|
|
||||||
|
gildedRose.updateQuality();
|
||||||
|
|
||||||
|
expect(getUpdateBehaviorMock).toHaveBeenCalledWith(item1);
|
||||||
|
expect(getUpdateBehaviorMock).toHaveBeenCalledWith(item2);
|
||||||
|
|
||||||
|
expect(updateMock).toHaveBeenCalledWith(item1);
|
||||||
|
expect(updateMock).toHaveBeenCalledWith(item2);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,16 +1,9 @@
|
|||||||
export class Item {
|
import { Item } from "@app/item";
|
||||||
name: string;
|
import { getUpdateBehaviorFor } from "@app/update-behaviors";
|
||||||
sellIn: number;
|
|
||||||
quality: number;
|
|
||||||
|
|
||||||
constructor(name, sellIn, quality) {
|
|
||||||
this.name = name;
|
|
||||||
this.sellIn = sellIn;
|
|
||||||
this.quality = quality;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class GildedRose {
|
export class GildedRose {
|
||||||
|
// also can't edit this because of the kata rules.
|
||||||
|
// But I prefer typing this as : Item[]
|
||||||
items: Array<Item>;
|
items: Array<Item>;
|
||||||
|
|
||||||
constructor(items = [] as Array<Item>) {
|
constructor(items = [] as Array<Item>) {
|
||||||
@ -18,52 +11,8 @@ export class GildedRose {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateQuality() {
|
updateQuality() {
|
||||||
for (let i = 0; i < this.items.length; i++) {
|
return this.items.map((item) => {
|
||||||
if (this.items[i].name != 'Aged Brie' && this.items[i].name != 'Backstage passes to a TAFKAL80ETC concert') {
|
return getUpdateBehaviorFor(item).update();
|
||||||
if (this.items[i].quality > 0) {
|
});
|
||||||
if (this.items[i].name != 'Sulfuras, Hand of Ragnaros') {
|
|
||||||
this.items[i].quality = this.items[i].quality - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (this.items[i].quality < 50) {
|
|
||||||
this.items[i].quality = this.items[i].quality + 1
|
|
||||||
if (this.items[i].name == 'Backstage passes to a TAFKAL80ETC concert') {
|
|
||||||
if (this.items[i].sellIn < 11) {
|
|
||||||
if (this.items[i].quality < 50) {
|
|
||||||
this.items[i].quality = this.items[i].quality + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.items[i].sellIn < 6) {
|
|
||||||
if (this.items[i].quality < 50) {
|
|
||||||
this.items[i].quality = this.items[i].quality + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.items[i].name != 'Sulfuras, Hand of Ragnaros') {
|
|
||||||
this.items[i].sellIn = this.items[i].sellIn - 1;
|
|
||||||
}
|
|
||||||
if (this.items[i].sellIn < 0) {
|
|
||||||
if (this.items[i].name != 'Aged Brie') {
|
|
||||||
if (this.items[i].name != 'Backstage passes to a TAFKAL80ETC concert') {
|
|
||||||
if (this.items[i].quality > 0) {
|
|
||||||
if (this.items[i].name != 'Sulfuras, Hand of Ragnaros') {
|
|
||||||
this.items[i].quality = this.items[i].quality - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.items[i].quality = this.items[i].quality - this.items[i].quality
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (this.items[i].quality < 50) {
|
|
||||||
this.items[i].quality = this.items[i].quality + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.items;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
TypeScript/app/item.ts
Normal file
18
TypeScript/app/item.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export class Item {
|
||||||
|
name: string;
|
||||||
|
sellIn: number;
|
||||||
|
quality: number;
|
||||||
|
|
||||||
|
// can't edit this constructor because of the kata rules.
|
||||||
|
// but I would change the constructor to take a javaScript object.
|
||||||
|
// It makes the consumer code more readable.
|
||||||
|
// e.g. new Item({ name: "standard item", sellIn: 0, quality: 2 })
|
||||||
|
// instead of new Item("standard item", 0, 2)
|
||||||
|
|
||||||
|
// I would also type the parameters, because now they are implicitly any.
|
||||||
|
constructor(name, sellIn, quality) {
|
||||||
|
this.name = name;
|
||||||
|
this.sellIn = sellIn;
|
||||||
|
this.quality = quality;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
TypeScript/app/update-behaviors/behavior-resolver.spec.ts
Normal file
40
TypeScript/app/update-behaviors/behavior-resolver.spec.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { Item } from "@app/item";
|
||||||
|
import { getUpdateBehaviorFor } from "./behavior-resolver";
|
||||||
|
import { AgedBrieBehavior } from "./implementations/aged-brie/aged-brie-behavior";
|
||||||
|
import { DefaultBehavior } from "./implementations/default/default-behavior";
|
||||||
|
import { BackstagePassBehavior } from "./implementations/backstage-pass/backstage-pass-behavior";
|
||||||
|
import { LegendaryItemBehavior } from "./implementations/legendary-item/legendary-item-behavior";
|
||||||
|
import { ConjuredItemBehavior } from "./implementations/conjured-item/conjured-item-behavior";
|
||||||
|
|
||||||
|
describe("Behavior resolver", () => {
|
||||||
|
it("should correctly resolve Aged Brie", () => {
|
||||||
|
expect(getUpdateBehaviorFor(new Item("Aged Brie", 0, 0))).toBeInstanceOf(
|
||||||
|
AgedBrieBehavior
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly resolve Backstage Passes", () => {
|
||||||
|
expect(
|
||||||
|
getUpdateBehaviorFor(
|
||||||
|
new Item("Backstage passes to a TAFKAL80ETC concert", 0, 0)
|
||||||
|
)
|
||||||
|
).toBeInstanceOf(BackstagePassBehavior);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly resolve Legendary Items", () => {
|
||||||
|
expect(
|
||||||
|
getUpdateBehaviorFor(new Item("Sulfuras, Hand of Ragnaros", 0, 0))
|
||||||
|
).toBeInstanceOf(LegendaryItemBehavior);
|
||||||
|
});
|
||||||
|
it("should correctly resolve Conjured Items", () => {
|
||||||
|
expect(
|
||||||
|
getUpdateBehaviorFor(new Item("Conjured Mana Cake", 0, 0))
|
||||||
|
).toBeInstanceOf(ConjuredItemBehavior);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly resolve the rest to Legacy behavior", () => {
|
||||||
|
expect(
|
||||||
|
getUpdateBehaviorFor(new Item("some other item", 0, 0))
|
||||||
|
).toBeInstanceOf(DefaultBehavior);
|
||||||
|
});
|
||||||
|
});
|
||||||
22
TypeScript/app/update-behaviors/behavior-resolver.ts
Normal file
22
TypeScript/app/update-behaviors/behavior-resolver.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Item } from "@app/item";
|
||||||
|
import { IUpdateBehavior } from "./update-behavior.interface";
|
||||||
|
import { DefaultBehavior } from "./implementations/default/default-behavior";
|
||||||
|
import { AgedBrieBehavior } from "./implementations/aged-brie/aged-brie-behavior";
|
||||||
|
import { BackstagePassBehavior } from "./implementations/backstage-pass/backstage-pass-behavior";
|
||||||
|
import { LegendaryItemBehavior } from "./implementations/legendary-item/legendary-item-behavior";
|
||||||
|
import { ConjuredItemBehavior } from "./implementations/conjured-item/conjured-item-behavior";
|
||||||
|
|
||||||
|
export function getUpdateBehaviorFor(item: Item): IUpdateBehavior {
|
||||||
|
switch (item.name) {
|
||||||
|
case "Aged Brie":
|
||||||
|
return new AgedBrieBehavior(item);
|
||||||
|
case "Backstage passes to a TAFKAL80ETC concert":
|
||||||
|
return new BackstagePassBehavior(item);
|
||||||
|
case "Sulfuras, Hand of Ragnaros":
|
||||||
|
return new LegendaryItemBehavior(item);
|
||||||
|
case "Conjured Mana Cake":
|
||||||
|
return new ConjuredItemBehavior(item);
|
||||||
|
default:
|
||||||
|
return new DefaultBehavior(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
import { Item } from "@app/item";
|
||||||
|
import { AgedBrieBehavior } from "./aged-brie-behavior";
|
||||||
|
|
||||||
|
describe("AgedBrie Behavior", () => {
|
||||||
|
it("should increase quality of Aged Brie as it gets older", () => {
|
||||||
|
const behavior = new AgedBrieBehavior(new Item("Aged Brie", 1, 1));
|
||||||
|
|
||||||
|
const result = behavior.update();
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
name: "Aged Brie",
|
||||||
|
sellIn: 0,
|
||||||
|
quality: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should increase quality of Aged Brie twice as fast after sell in date", () => {
|
||||||
|
const behavior = new AgedBrieBehavior(new Item("Aged Brie", 0, 1));
|
||||||
|
|
||||||
|
const result = behavior.update();
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
name: "Aged Brie",
|
||||||
|
sellIn: -1,
|
||||||
|
quality: 3,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should never increase quality of Aged Brie over 50", () => {
|
||||||
|
const behavior = new AgedBrieBehavior(new Item("Aged Brie", 0, 50));
|
||||||
|
|
||||||
|
const result = behavior.update();
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
name: "Aged Brie",
|
||||||
|
sellIn: -1,
|
||||||
|
quality: 50,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { config } from "@app/config";
|
||||||
|
import { Item } from "@app/item";
|
||||||
|
import { IUpdateBehavior } from "@app/update-behaviors";
|
||||||
|
|
||||||
|
export class AgedBrieBehavior implements IUpdateBehavior {
|
||||||
|
constructor(private item: Item) {}
|
||||||
|
|
||||||
|
update(): Item {
|
||||||
|
const isPastSellInDay = this.item.sellIn <= 0;
|
||||||
|
|
||||||
|
const amountToAdd = isPastSellInDay ? 2 : 1;
|
||||||
|
|
||||||
|
this.item.quality = Math.min(
|
||||||
|
config.maxQuality,
|
||||||
|
this.item.quality + amountToAdd
|
||||||
|
);
|
||||||
|
|
||||||
|
this.item.sellIn -= 1;
|
||||||
|
|
||||||
|
return this.item;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
import { Item } from "@app/item";
|
||||||
|
import { BackstagePassBehavior } from "./backstage-pass-behavior";
|
||||||
|
|
||||||
|
describe("Backstage Pass Behavior", () => {
|
||||||
|
it("should increase quality of Backstage passes by 1 if sell in date is more than 10 days away", () => {
|
||||||
|
const behavior = new BackstagePassBehavior(
|
||||||
|
new Item("Backstage passes to a TAFKAL80ETC concert", 11, 20)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = behavior.update();
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
name: "Backstage passes to a TAFKAL80ETC concert",
|
||||||
|
sellIn: 10,
|
||||||
|
quality: 21,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should increase quality of Backstage passes by 2 if sell in date is less than 10 days away but more than 5", () => {
|
||||||
|
const behavior = new BackstagePassBehavior(
|
||||||
|
new Item("Backstage passes to a TAFKAL80ETC concert", 9, 20)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = behavior.update();
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
name: "Backstage passes to a TAFKAL80ETC concert",
|
||||||
|
sellIn: 8,
|
||||||
|
quality: 22,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should increase quality of Backstage passes by 3 if sell in date is less than 5 days away", () => {
|
||||||
|
const behavior = new BackstagePassBehavior(
|
||||||
|
new Item("Backstage passes to a TAFKAL80ETC concert", 4, 20)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = behavior.update();
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
name: "Backstage passes to a TAFKAL80ETC concert",
|
||||||
|
sellIn: 3,
|
||||||
|
quality: 23,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should drop quality of Backstage passes to 0 after sell in date", () => {
|
||||||
|
const behavior = new BackstagePassBehavior(
|
||||||
|
new Item("Backstage passes to a TAFKAL80ETC concert", 0, 20)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = behavior.update();
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
name: "Backstage passes to a TAFKAL80ETC concert",
|
||||||
|
sellIn: -1,
|
||||||
|
quality: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not increase over 50", () => {
|
||||||
|
const behavior = new BackstagePassBehavior(
|
||||||
|
new Item("Backstage passes to a TAFKAL80ETC concert", 4, 50)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = behavior.update();
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
name: "Backstage passes to a TAFKAL80ETC concert",
|
||||||
|
sellIn: 3,
|
||||||
|
quality: 50,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
import { config } from "@app/config";
|
||||||
|
import { Item } from "@app/item";
|
||||||
|
import { IUpdateBehavior } from "@app/update-behaviors";
|
||||||
|
|
||||||
|
export class BackstagePassBehavior implements IUpdateBehavior {
|
||||||
|
constructor(private item: Item) {}
|
||||||
|
update(): Item {
|
||||||
|
const sellIn = this.item.sellIn;
|
||||||
|
const amountToIncrease = this.#getAmountToIncrease(sellIn);
|
||||||
|
|
||||||
|
this.item.quality = Math.min(
|
||||||
|
this.item.quality + amountToIncrease,
|
||||||
|
config.maxQuality
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sellIn <= 0) {
|
||||||
|
this.item.quality = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.item.sellIn -= 1;
|
||||||
|
|
||||||
|
return this.item;
|
||||||
|
}
|
||||||
|
|
||||||
|
#getAmountToIncrease(sellIn: number): number {
|
||||||
|
if (sellIn <= 5) return 3;
|
||||||
|
if (sellIn <= 10) return 2;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
import { Item } from "@app/item";
|
||||||
|
import { ConjuredItemBehavior } from "./conjured-item-behavior";
|
||||||
|
|
||||||
|
describe("Conjured Item Behavior", () => {
|
||||||
|
it("should degrade quality with 2 and sellIn with 1 when sellIn is over 0", () => {
|
||||||
|
const behavior = new ConjuredItemBehavior(
|
||||||
|
new Item("Conjured Mana Cake", 1, 4)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = behavior.update();
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
name: "Conjured Mana Cake",
|
||||||
|
sellIn: 0,
|
||||||
|
quality: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("should degrade quality with 4 and sellin with 1 when sellIn is equal to zero", () => {
|
||||||
|
const behavior = new ConjuredItemBehavior(
|
||||||
|
new Item("Conjured Mana Cake", 0, 5)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = behavior.update();
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
name: "Conjured Mana Cake",
|
||||||
|
sellIn: -1,
|
||||||
|
quality: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should degrade quality with 4 and sellin with 1 when sellIn is under zero", () => {
|
||||||
|
const behavior = new ConjuredItemBehavior(
|
||||||
|
new Item("Conjured Mana Cake", -1, 5)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = behavior.update();
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
name: "Conjured Mana Cake",
|
||||||
|
sellIn: -2,
|
||||||
|
quality: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shouldn't degrade quality under 0", () => {
|
||||||
|
const behavior = new ConjuredItemBehavior(
|
||||||
|
new Item("Conjured Mana Cake", 1, 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = behavior.update();
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
name: "Conjured Mana Cake",
|
||||||
|
sellIn: 0,
|
||||||
|
quality: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { Item } from "@app/item";
|
||||||
|
import { IUpdateBehavior } from "@app/update-behaviors/update-behavior.interface";
|
||||||
|
|
||||||
|
export class ConjuredItemBehavior implements IUpdateBehavior {
|
||||||
|
constructor(private item: Item) {}
|
||||||
|
|
||||||
|
update(): Item {
|
||||||
|
const amountToSubtract = this.item.sellIn <= 0 ? 4 : 2;
|
||||||
|
|
||||||
|
this.item.quality = Math.max(this.item.quality - amountToSubtract, 0);
|
||||||
|
|
||||||
|
this.item.sellIn -= 1;
|
||||||
|
|
||||||
|
return this.item;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
import { Item } from "@app/item";
|
||||||
|
import { DefaultBehavior } from "./default-behavior";
|
||||||
|
|
||||||
|
describe("Default Behavior", () => {
|
||||||
|
it("should degrade sell inn and quality each day", () => {
|
||||||
|
const behavior = new DefaultBehavior(new Item("standard item", 1, 1));
|
||||||
|
|
||||||
|
const result = behavior.update();
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
name: "standard item",
|
||||||
|
sellIn: 0,
|
||||||
|
quality: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should degrade quality twice as fast after sell in date", () => {
|
||||||
|
const behavior = new DefaultBehavior(new Item("standard item", 0, 2));
|
||||||
|
|
||||||
|
const result = behavior.update();
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
name: "standard item",
|
||||||
|
sellIn: -1,
|
||||||
|
quality: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not degrade quality below 0", () => {
|
||||||
|
const behavior = new DefaultBehavior(new Item("standard item", 1, 0));
|
||||||
|
|
||||||
|
const result = behavior.update();
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
name: "standard item",
|
||||||
|
sellIn: 0,
|
||||||
|
quality: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
import { Item } from "@app/item";
|
||||||
|
import { IUpdateBehavior } from "../../update-behavior.interface";
|
||||||
|
|
||||||
|
export class DefaultBehavior implements IUpdateBehavior {
|
||||||
|
constructor(private item: Item) {}
|
||||||
|
|
||||||
|
update(): Item {
|
||||||
|
const amountToSubtract = this.item.sellIn <= 0 ? 2 : 1;
|
||||||
|
this.item.quality = Math.max(this.item.quality - amountToSubtract, 0);
|
||||||
|
|
||||||
|
this.item.sellIn -= 1;
|
||||||
|
|
||||||
|
return this.item;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { config } from "@app/config";
|
||||||
|
import { Item } from "@app/item";
|
||||||
|
import { IUpdateBehavior } from "@app/update-behaviors/update-behavior.interface";
|
||||||
|
|
||||||
|
export class LegendaryItemBehavior implements IUpdateBehavior {
|
||||||
|
constructor(public item: Item) {}
|
||||||
|
update(): Item {
|
||||||
|
if (this.item.quality !== 80) {
|
||||||
|
throw new Error("A Legendary Item cannot have a quality other than 80");
|
||||||
|
}
|
||||||
|
return this.item;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
import { Item } from "@app/item";
|
||||||
|
import { LegendaryItemBehavior } from "./legendary-item-behavior";
|
||||||
|
|
||||||
|
describe("Legendary Item Behavior", () => {
|
||||||
|
it("should keep items of quality 80 the same", () => {
|
||||||
|
const behavior = new LegendaryItemBehavior(
|
||||||
|
new Item("Sulfuras, Hand of Ragnaros", 0, 80)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = behavior.update();
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
name: "Sulfuras, Hand of Ragnaros",
|
||||||
|
sellIn: 0,
|
||||||
|
quality: 80,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error when a legendary item doesn't have a quality of 80", () => {
|
||||||
|
const behavior = new LegendaryItemBehavior(
|
||||||
|
new Item("Sulfuras, Hand of Ragnaros", 0, 5)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(() => behavior.update()).toThrow(
|
||||||
|
"A Legendary Item cannot have a quality other than 80"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
2
TypeScript/app/update-behaviors/index.ts
Normal file
2
TypeScript/app/update-behaviors/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./update-behavior.interface";
|
||||||
|
export * from "./behavior-resolver";
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
import { Item } from "@app/item";
|
||||||
|
|
||||||
|
export interface IUpdateBehavior {
|
||||||
|
update: () => Item;
|
||||||
|
}
|
||||||
@ -1,13 +1,15 @@
|
|||||||
import { pathsToModuleNameMapper } from "ts-jest";
|
import { pathsToModuleNameMapper } from "ts-jest";
|
||||||
import { compilerOptions } from './tsconfig.json'
|
import { compilerOptions } from "./tsconfig.json";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
roots: ['<rootDir>/app', '<rootDir>/test/jest'],
|
roots: ["<rootDir>/app", "<rootDir>/snapshot-tests"],
|
||||||
collectCoverage: true,
|
collectCoverage: true,
|
||||||
coverageDirectory: 'coverage',
|
coverageDirectory: "coverage",
|
||||||
coverageProvider: 'v8',
|
coverageProvider: "v8",
|
||||||
transform: {
|
transform: {
|
||||||
'^.+\\.tsx?$': 'ts-jest',
|
"^.+\\.tsx?$": "ts-jest",
|
||||||
},
|
},
|
||||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '<rootDir>/' } ),
|
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, {
|
||||||
|
prefix: "<rootDir>/",
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,33 +3,23 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Gilded Rose kata in TypeScript",
|
"description": "Gilded Rose kata in TypeScript",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"precompile": "rimraf app/**/*.js test/**/*.js",
|
"precompile": "rimraf app/**/*.js snapshot-tests/**/*.js",
|
||||||
"compile": "tsc",
|
"compile": "tsc",
|
||||||
"pretest": "rimraf app/**/*.js test/**/*.js",
|
"pretest": "rimraf app/**/*.js snapshot-tests/**/*.js",
|
||||||
"test:jest": "jest",
|
"test": "jest",
|
||||||
"test:jest:watch": "jest --watchAll",
|
"test:watch": "jest --watchAll"
|
||||||
"test:mocha": "nyc mocha",
|
|
||||||
"test:vitest": "vitest --coverage"
|
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": true,
|
"private": true,
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chai": "^4.2.22",
|
|
||||||
"@types/jest": "^29.4.0",
|
"@types/jest": "^29.4.0",
|
||||||
"@types/mocha": "^10.0.1",
|
|
||||||
"@types/node": "^18.14.0",
|
"@types/node": "^18.14.0",
|
||||||
"@vitest/coverage-istanbul": "^0.28.5",
|
|
||||||
"chai": "^4.3.4",
|
|
||||||
"jest": "^29.4.3",
|
"jest": "^29.4.3",
|
||||||
"mocha": "^10.2.0",
|
|
||||||
"nyc": "~15.1.0",
|
|
||||||
"rimraf": "^4.1.2",
|
"rimraf": "^4.1.2",
|
||||||
"source-map-support": "^0.5.20",
|
"source-map-support": "^0.5.20",
|
||||||
"ts-jest": "^29.0.5",
|
"ts-jest": "^29.0.5",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"tsconfig-paths": "^4.1.2",
|
"tsconfig-paths": "^4.1.2",
|
||||||
"typescript": "^4.4.4",
|
"typescript": "^4.4.4"
|
||||||
"vite-tsconfig-paths": "^4.0.5",
|
|
||||||
"vitest": "^0.28.5"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1501
TypeScript/snapshot-tests/__snapshots__/approvals.spec.ts.snap
Normal file
1501
TypeScript/snapshot-tests/__snapshots__/approvals.spec.ts.snap
Normal file
File diff suppressed because it is too large
Load Diff
28
TypeScript/snapshot-tests/approvals.spec.ts
Normal file
28
TypeScript/snapshot-tests/approvals.spec.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { GildedRose } from "@app/gilded-rose";
|
||||||
|
import { Item } from "@app/item";
|
||||||
|
|
||||||
|
describe("Gilded Rose Approval", () => {
|
||||||
|
it("should match the snapshot for thirty Days", () => {
|
||||||
|
const items = [
|
||||||
|
new Item("+5 Dexterity Vest", 10, 20), //
|
||||||
|
new Item("Aged Brie", 2, 0), //
|
||||||
|
new Item("Elixir of the Mongoose", 5, 7), //
|
||||||
|
new Item("Sulfuras, Hand of Ragnaros", 0, 80), //
|
||||||
|
new Item("Sulfuras, Hand of Ragnaros", -1, 80),
|
||||||
|
new Item("Backstage passes to a TAFKAL80ETC concert", 15, 20),
|
||||||
|
new Item("Backstage passes to a TAFKAL80ETC concert", 10, 49),
|
||||||
|
new Item("Backstage passes to a TAFKAL80ETC concert", 5, 49),
|
||||||
|
// this conjured item does not work properly yet
|
||||||
|
new Item("Conjured Mana Cake", 3, 6),
|
||||||
|
];
|
||||||
|
|
||||||
|
const gildedRose = new GildedRose(items);
|
||||||
|
|
||||||
|
const days = 30;
|
||||||
|
|
||||||
|
for (let i = 0; i < days; i++) {
|
||||||
|
const updatedItems = gildedRose.updateQuality();
|
||||||
|
expect(updatedItems).toMatchSnapshot();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,34 +0,0 @@
|
|||||||
import { Item, GildedRose } from '../app/gilded-rose';
|
|
||||||
|
|
||||||
console.log("OMGHAI!")
|
|
||||||
|
|
||||||
const items = [
|
|
||||||
new Item("+5 Dexterity Vest", 10, 20), //
|
|
||||||
new Item("Aged Brie", 2, 0), //
|
|
||||||
new Item("Elixir of the Mongoose", 5, 7), //
|
|
||||||
new Item("Sulfuras, Hand of Ragnaros", 0, 80), //
|
|
||||||
new Item("Sulfuras, Hand of Ragnaros", -1, 80),
|
|
||||||
new Item("Backstage passes to a TAFKAL80ETC concert", 15, 20),
|
|
||||||
new Item("Backstage passes to a TAFKAL80ETC concert", 10, 49),
|
|
||||||
new Item("Backstage passes to a TAFKAL80ETC concert", 5, 49),
|
|
||||||
// this conjured item does not work properly yet
|
|
||||||
new Item("Conjured Mana Cake", 3, 6)];
|
|
||||||
|
|
||||||
|
|
||||||
const gildedRose = new GildedRose(items);
|
|
||||||
|
|
||||||
let days: number = 2;
|
|
||||||
if (process.argv.length > 2) {
|
|
||||||
days = +process.argv[2];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < days + 1; i++) {
|
|
||||||
console.log("-------- day " + i + " --------");
|
|
||||||
console.log("name, sellIn, quality");
|
|
||||||
items.forEach(element => {
|
|
||||||
console.log(element.name + ', ' + element.sellIn + ', ' + element.quality);
|
|
||||||
|
|
||||||
});
|
|
||||||
console.log();
|
|
||||||
gildedRose.updateQuality();
|
|
||||||
}
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
import { Item, GildedRose } from '@/gilded-rose';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This unit test uses [Jest Snapshot](https://goo.gl/fbAQLP).
|
|
||||||
*
|
|
||||||
* There are two test cases here with different styles:
|
|
||||||
* <li>"foo" is more similar to the unit test from the 'Java' version
|
|
||||||
* <li>"thirtyDays" is more similar to the TextTest from the 'Java' version
|
|
||||||
*
|
|
||||||
* I suggest choosing one style to develop and deleting the other.
|
|
||||||
*/
|
|
||||||
|
|
||||||
describe('Gilded Rose Approval', () => {
|
|
||||||
|
|
||||||
let gameConsoleOutput: string;
|
|
||||||
let originalConsoleLog: (message: any) => void;
|
|
||||||
let originalProcessArgv: string[]
|
|
||||||
|
|
||||||
function gameConsoleLog(msg: string) {
|
|
||||||
if (msg) {
|
|
||||||
gameConsoleOutput += msg;
|
|
||||||
}
|
|
||||||
gameConsoleOutput += "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
// prepare capturing console.log to our own gameConsoleLog.
|
|
||||||
gameConsoleOutput = "";
|
|
||||||
originalConsoleLog = console.log;
|
|
||||||
console.log = gameConsoleLog;
|
|
||||||
originalProcessArgv = process.argv;
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
// reset original console.log
|
|
||||||
console.log = originalConsoleLog;
|
|
||||||
process.argv = originalProcessArgv;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should foo', () => {
|
|
||||||
const gildedRose = new GildedRose([new Item('foo', 0, 0)]);
|
|
||||||
const items = gildedRose.updateQuality();
|
|
||||||
|
|
||||||
expect(items).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should thirtyDays', () => {
|
|
||||||
process.argv = ["<node>", "<script", "30"];
|
|
||||||
require('../golden-master-text-test.ts');
|
|
||||||
|
|
||||||
expect(gameConsoleOutput).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
import { Item, GildedRose } from '@/gilded-rose';
|
|
||||||
|
|
||||||
describe('Gilded Rose', () => {
|
|
||||||
it('should foo', () => {
|
|
||||||
const gildedRose = new GildedRose([new Item('foo', 0, 0)]);
|
|
||||||
const items = gildedRose.updateQuality();
|
|
||||||
expect(items[0].name).toBe('fixme');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import { expect } from 'chai';
|
|
||||||
import { Item, GildedRose } from '@/gilded-rose';
|
|
||||||
|
|
||||||
describe('Gilded Rose', () => {
|
|
||||||
it('should foo', () => {
|
|
||||||
const gildedRose = new GildedRose([new Item('foo', 0, 0)]);
|
|
||||||
const items = gildedRose.updateQuality();
|
|
||||||
expect(items[0].name).to.equal('fixme');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
import { execSync } from 'node:child_process';
|
|
||||||
import { Item, GildedRose } from '@/gilded-rose';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This test uses Vitest Snapshot, similar to [Jest Snapshot](https://goo.gl/fbAQLP).
|
|
||||||
*
|
|
||||||
* There are two test cases here with different styles:
|
|
||||||
* <li>"foo" is more similar to the unit test from the 'Java' version
|
|
||||||
* <li>"thirtyDays" is more similar to the TextTest from the 'Java' version
|
|
||||||
*
|
|
||||||
* I suggest choosing one style to develop and deleting the other.
|
|
||||||
*/
|
|
||||||
|
|
||||||
describe('Gilded Rose Approval', () => {
|
|
||||||
it('should foo', () => {
|
|
||||||
const gildedRose = new GildedRose([new Item('foo', 0, 0)]);
|
|
||||||
const items = gildedRose.updateQuality();
|
|
||||||
|
|
||||||
expect(items).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should thirtyDays', () => {
|
|
||||||
const consoleOutput = execSync(
|
|
||||||
'ts-node test/golden-master-text-test.ts 30',
|
|
||||||
{ encoding: 'utf-8' }
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(consoleOutput).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
import { Item, GildedRose } from '@/gilded-rose';
|
|
||||||
|
|
||||||
describe('Gilded Rose', () => {
|
|
||||||
it('should foo', () => {
|
|
||||||
const gildedRose = new GildedRose([new Item('foo', 0, 0)]);
|
|
||||||
const items = gildedRose.updateQuality();
|
|
||||||
expect(items[0].name).toBe('fixme');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
"""
|
|
||||||
This script uses npx to execute the TextTest Fixture for TypeScript.
|
|
||||||
It is designed to be used by TextTest and specified in the file 'texttests/config.gr' in this repo.
|
|
||||||
It is more convenient for TextTest to use since npx needs
|
|
||||||
several arguments in addition to the one the TextTest fixture needs.
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
args = " ".join(sys.argv[1:])
|
|
||||||
TEXTTEST_HOME = os.environ.get("TEXTTEST_HOME", os.getcwd())
|
|
||||||
subprocess.run(f"npx ts-node {TEXTTEST_HOME}/TypeScript/test/golden-master-text-test.ts {args}", shell=True)
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es5",
|
"target": "es2022",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
@ -9,12 +9,8 @@
|
|||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@app/*": ["app/*"]
|
||||||
"app/*"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": ["node_modules"]
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
import {defineConfig} from 'vitest/config';
|
|
||||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [tsconfigPaths()],
|
|
||||||
test: {
|
|
||||||
globals: true,
|
|
||||||
include: ['test/vitest/**/*.{spec,test}.{js,ts}'],
|
|
||||||
coverage: {
|
|
||||||
provider: 'istanbul',
|
|
||||||
reporter: ['text', 'html']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
Loading…
Reference in New Issue
Block a user