-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: tags, tell, story, test, fix intellisense
- [x] feat: add tags to story print - [x] refactor: rename types around Story concept for more ergonomic API - [x] feat: added test field for stories - [x] refactor: improved Typescript intellisense and DX by providing StoryScript using `satisfies` - [x] refactor: split into individual files. - [x] chore: added eslint - [x] feat: added common tag types as stories. - [ ] WIP: adding YAML Support
- Loading branch information
Showing
33 changed files
with
1,523 additions
and
463 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
module.exports = { | ||
"env": { | ||
"browser": true, | ||
"es2021": true | ||
}, | ||
"extends": [ | ||
"eslint:recommended", | ||
"plugin:@typescript-eslint/recommended", | ||
], | ||
"overrides": [ | ||
{ | ||
"env": { | ||
"node": true | ||
}, | ||
"files": [ | ||
".eslintrc.{js,cjs}" | ||
], | ||
"parserOptions": { | ||
"sourceType": "script" | ||
} | ||
} | ||
], | ||
"parser": "@typescript-eslint/parser", | ||
"parserOptions": { | ||
"ecmaVersion": "latest", | ||
"sourceType": "module" | ||
}, | ||
"plugins": [ | ||
"@typescript-eslint", | ||
], | ||
"rules": { | ||
"@typescript-eslint/no-unused-vars": "off", | ||
"@typescript-eslint/no-explicit-any": "off", | ||
}, | ||
"settings": { | ||
|
||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import {Story, TestKind} from "@src/act/stories.ts"; | ||
import {Genres} from "@src/act/story-kinds.ts"; | ||
import {Scenes} from "@src/entrance.ts"; | ||
import {IStoryScripts} from "@src/act/story-types.ts"; | ||
import {StoryStatus} from "@src/act/status.ts"; | ||
import {StoryOptions} from "@src/act/story-options.ts"; | ||
|
||
export interface Test { | ||
/** | ||
* The type of tests. | ||
*/ | ||
kind: TestKind; | ||
/** | ||
* The condition when the test is considered done. | ||
*/ | ||
doneWhen?: string; | ||
/** | ||
* The issues that this test it closes. | ||
*/ | ||
closeIssues?: string[]; | ||
|
||
} | ||
/** | ||
* Bare Act is the base structure for any entity or behavior without methods or nested entities. | ||
*/ | ||
export interface IStoryScript { | ||
/** | ||
* Description of the entity. | ||
* | ||
* @remarks | ||
* This field is intended to be used as the description of a test. | ||
* According to the actual type of entity, it should be worded accordingly. | ||
* | ||
* @example | ||
* ```ts | ||
* // For an entity, it should be worded as a noun. | ||
* const aDogEntity = createEntity({ | ||
* describe: "a dog", | ||
* }) | ||
* | ||
* // For a behavior, it should be worded as a verb or an expectation. | ||
* const shouldBark = createBehavior({ | ||
* describe: "should bark", // alternatively: "barks", "can bark", "barking", etc. | ||
* action: () => { | ||
* // ... | ||
* } | ||
* }) | ||
* ``` | ||
*/ | ||
story: string; | ||
/** | ||
* Story-by-Story options that can override the global story options. | ||
*/ | ||
options?: StoryOptions; | ||
genre?: Genres; | ||
tests?: Record<string, Test> | ||
/** | ||
* The name of a subject in this act. | ||
* | ||
* @remarks | ||
* When used in full description of the act, the subject, if provided, will be inserted as the subject of the description. | ||
*/ | ||
protagonist?: string; | ||
parentPath?: string; | ||
explain?: string; | ||
/** | ||
* The condition when the story is considered done. | ||
*/ | ||
done?: string; | ||
/** | ||
* The current status of the story | ||
*/ | ||
status?: StoryStatus | string; | ||
lastUpdate?: string; | ||
|
||
priority?: number; | ||
|
||
context?: IStoryScript[]; | ||
when?: IStoryScript[]; | ||
then?: IStoryScript[]; | ||
scenes?: IStoryScripts; | ||
tellAs?: (fn: (entity: Story) => string) => string; | ||
} | ||
|
||
export interface IStory extends IStoryScript { | ||
scenes: Scenes; | ||
readonly testId: () => string; | ||
path: () => string; | ||
nextActToDo: () => Story | undefined; | ||
|
||
/** | ||
* Simply tell the story, using teller preference {@link StoryTellingOptions} to decide whether to tell the short or long version. | ||
*/ | ||
tell: () => string; | ||
|
||
/** | ||
* Tell the shorter version of the story using the story text. | ||
*/ | ||
short: () => string; | ||
/** | ||
* Tell the longer version of the story using the `contexts`, `when`, and `then`, in the form of, for example, `Given ... When ... Then ...`. | ||
* | ||
* @remarks | ||
* If a long description is not provided, it will fallback to the short version. | ||
*/ | ||
long: () => string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
export enum StoryStatus { | ||
NO_STATUS = 'NO_STATUS', | ||
BACKLOG = 'BACKLOG', | ||
DESIGN = 'DESIGN', | ||
IN_PROGRESS = 'IN_PROGRESS', | ||
REVIEW = 'REVIEW', | ||
DONE = 'DONE', | ||
ARCHIVED = 'ARCHIVED', | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
import {PartialDeep, ReadonlyDeep} from "type-fest"; | ||
import {ACT_DEFAULT_DESCRIPTIONS} from "@src/consts.ts"; | ||
import {Scenes} from "@src/entrance.ts"; | ||
|
||
import {IStory, IStoryScript, Test} from "@src/act/interfaces.ts"; | ||
import {Genres} from "@src/act/story-kinds.ts"; | ||
import {getPath, populateActPath} from "@src/act/utils.ts"; | ||
import {TestKind} from "@src/act/test-kinds.ts"; | ||
import {printTag, printTestTags, tellStory} from "@src/act/storyteller.ts"; | ||
import {IStoryScripts} from "@src/act/story-types.ts"; | ||
import {NameList} from "@src/util-types.ts"; | ||
import {StoryOptions, StoryVersion} from "@src/act/story-options.ts"; | ||
import {StoryStatus} from "@src/act/status.ts"; | ||
|
||
|
||
class StoryScript implements IStoryScript { | ||
story: string = ACT_DEFAULT_DESCRIPTIONS.DEFAULT_NARRATIVE; | ||
parentPath?: string | undefined; | ||
genre?: Genres = Genres.ACT; | ||
implemented?: boolean = false; | ||
protagonist?: string = "it"; | ||
explain?: string; | ||
options?: StoryOptions; | ||
|
||
constructor( | ||
entity: Partial<IStoryScript>, | ||
opt?: StoryOptions, | ||
) { | ||
if (opt?.defaultKind) { | ||
this.genre = opt.defaultKind; | ||
} | ||
Object.assign(this, entity); | ||
|
||
|
||
} | ||
} | ||
|
||
export class Story extends StoryScript implements IStory { | ||
options?: StoryOptions; | ||
scenes: Scenes = {}; | ||
context?: StoryScript[]; | ||
when?: StoryScript[]; | ||
then?: StoryScript[]; | ||
status?: StoryStatus | string; | ||
priority?: number; | ||
|
||
path = () => getPath(this.story, this.parentPath) | ||
// get a getter to get test id | ||
testId = this.path; | ||
tellAs: (fn: (entity: Story) => string) => string; | ||
|
||
nextActToDo(): Story | undefined { | ||
return undefined | ||
} | ||
|
||
long = () => tellStory(this, StoryVersion.LONG); | ||
|
||
short = () => tellStory(this, StoryVersion.SHORT) | ||
tell = () => tellStory(this, StoryVersion.NO_PREFERENCE); | ||
|
||
/** | ||
* Tell the story but with test kinds as tag prefixes, e.g. `[UNIT] [E2E] Story` | ||
* @param tests | ||
*/ | ||
tellForTest = (tests: Test[]) => [printTestTags(tests), tellStory(this, StoryVersion.NO_PREFERENCE)].join(' '); | ||
|
||
/** | ||
* Print test's kinds as tags, e.g. `[UNIT] [E2E]` | ||
*/ | ||
printTestTags = (tests: Test[]) => printTestTags(tests); | ||
|
||
/** | ||
* Print the story as a tag, e.g. `[STORY]` | ||
* | ||
*/ | ||
printAsTag = () => printTag(this.tell()); | ||
|
||
// Get the next act according to the priority and the implementation status of the act. | ||
|
||
|
||
constructor( | ||
entity: PartialDeep<StoryScript>, | ||
opt?: StoryOptions, | ||
) { | ||
// if the provided story script has individual options set, use it to override the global options for this story | ||
const storyOptions = entity.options || opt; | ||
|
||
super(entity, storyOptions); | ||
Object.assign(this, entity); | ||
|
||
// store options | ||
this.options = storyOptions; | ||
|
||
// populate entity | ||
populateActPath(this); | ||
|
||
// Use describeAs function if provided | ||
this.tellAs = (fn) => fn(this); | ||
|
||
} | ||
|
||
|
||
} | ||
|
||
/** | ||
* Type for acts created with user-defined act records. | ||
* | ||
* @remarks | ||
* Represents an enhanced version of an `IActRecord` with additional methods from the `Act` class. | ||
* This type recursively applies itself to each nested act within the `acts` property, ensuring that | ||
* each nested act also receives the benefit of intellisense. | ||
* | ||
* The `acts` property is a mapped type that iterates over each key in the original `acts` property | ||
* of the provided `IActRecord`. For each key, it checks if the corresponding value extends `IActRecord`. | ||
* If it does, the type is recursively applied to this value, enhancing it with `Act` methods. | ||
* If it does not extend `IActRecord`, the type for that key is set to `never`, indicating an invalid type. | ||
* | ||
* This approach allows the `UserAct` type to maintain the original structure of the `IActRecord`, | ||
* including any nested acts, while also adding the methods and properties defined in the `Act` class. | ||
* As a result, instances of `ActWithMethods` have both the data structure defined by their specific `IActRecord` | ||
* implementation and the functionality provided by `Act`. | ||
* | ||
* @template T - A type extending `IActRecord` that represents the structure of the act record. | ||
* This generic type allows `ActWithMethods` to be applied to any specific implementation | ||
* of `IActRecord`, preserving its unique structure. | ||
*/ | ||
export type UserStory<T extends IStoryScript> = ReadonlyDeep<T> & Story & { | ||
scenes: { [K in keyof T['scenes']]: T['scenes'][K] extends IStoryScript ? ReadonlyDeep<UserStory<T['scenes'][K]>> : never }; | ||
}; | ||
|
||
export type UserStories<T extends IStoryScripts> = ReadonlyDeep<{ [K in keyof T]: ReadonlyDeep<UserStory<T[K]>> }>; | ||
|
||
export type UserNameList<T extends NameList> = ReadonlyDeep<{ | ||
[K in keyof T]: ReadonlyDeep<UserStory<{ | ||
story: T[K] | ||
}>> | ||
}>; | ||
|
||
export {TestKind}; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
|
||
|
||
|
||
/** | ||
* Types for Act kinds. | ||
*/ | ||
export enum GenreEntity { | ||
ACT = "ACT", | ||
ENTITY = "ENTITY", | ||
BEHAVIOR = "BEHAVIOR", | ||
DOMAIN = "DOMAIN", | ||
BACKGROUND = "BACKGROUND", | ||
} | ||
|
||
|
||
export enum GenreGherkin { | ||
SCENARIO = "SCENARIO", | ||
GIVEN = "GIVEN", | ||
WHEN = "WHEN", | ||
THEN = "THEN", | ||
} | ||
|
||
export enum GenreUserStory { | ||
EPIC = "EPIC", | ||
STORY = "STORY", | ||
FEATURE = "FEAT", | ||
STEP = "STEP", | ||
AS_A_USER = "AS A USER", | ||
I_WANT_TO = "I WANT TO", | ||
SO_THAT = "SO THAT", | ||
} | ||
|
||
export type GenreBDD = GenreGherkin | GenreUserStory; | ||
|
||
export const Genres = { | ||
...GenreEntity, | ||
...GenreGherkin, | ||
...GenreUserStory, | ||
} | ||
|
||
export type Genres = GenreEntity | GenreBDD; |
Oops, something went wrong.