Skip to content

Commit

Permalink
feat: tags, tell, story, test, fix intellisense
Browse files Browse the repository at this point in the history
- [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
boan-anbo committed Nov 12, 2023
1 parent 33a13e4 commit 4232bbe
Show file tree
Hide file tree
Showing 33 changed files with 1,523 additions and 463 deletions.
38 changes: 38 additions & 0 deletions packages/test-acts/.eslintrc.cjs
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": {

}
}
9 changes: 8 additions & 1 deletion packages/test-acts/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Test Acts
# Cantos: Write Better Tests

Or, the poor men's Homer.

Test Acts is a minimalistic, framework-agnostic, typesafe, and lightweight (~3kb) test planner to support your TDD (
Test-driven Development) and BDD (Behavior-driven Development) workflows.
Expand Down Expand Up @@ -63,8 +65,13 @@ import {A} from 'test-acts'

## Examples

### Use satisfies to get intellisense for UserAct
```ts


```

- It works great with CoPilot because it has a clear structure to infer from.

## Glossary

Expand Down
15 changes: 12 additions & 3 deletions packages/test-acts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,25 @@
"devDependencies": {
"@types/node": "^20.9.0",
"@types/uuid": "^9.0.7",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"@vitest/coverage-v8": "^0.34.6",
"@vitest/ui": "^1.0.0-beta.4",
"eslint": "^8.53.0",
"type-fest": "^4.7.1",
"typescript": "^5.0.2",
"typescript": "^5",
"vite": "^4.4.5",
"vite-plugin-dts": "^3.6.3",
"vitest": "^1.0.0-beta.4"
"vite-plugin-eslint": "^1.8.1",
"vitest": "^1.0.0-beta.4",
"yaml": "^2.3.4"
},
"dependencies": {
"mermaid": "^10.6.1",
"uuid": "^9.0.1"
}
},
"peerDependencies": {
"typescript": "^5"
},
"license": "MIT"
}
107 changes: 107 additions & 0 deletions packages/test-acts/src/act/interfaces.ts
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;
}
9 changes: 9 additions & 0 deletions packages/test-acts/src/act/status.ts
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',
}
140 changes: 140 additions & 0 deletions packages/test-acts/src/act/stories.ts
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};

41 changes: 41 additions & 0 deletions packages/test-acts/src/act/story-kinds.ts
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;
Loading

0 comments on commit 4232bbe

Please sign in to comment.