From a0c72d884d180af869cfc29a772be6909d0b8fa3 Mon Sep 17 00:00:00 2001 From: Bo Date: Sun, 12 Nov 2023 19:43:01 -0500 Subject: [PATCH] chore: rename package name to cantos --- README.md | 1 - packages/cantos/.eslintrc.cjs | 38 ++++ packages/cantos/LICENSE.md | 21 ++ packages/cantos/README.md | 199 ++++++++++++++++++ packages/cantos/package.json | 50 +++++ packages/cantos/src/act/interfaces.ts | 106 ++++++++++ packages/cantos/src/act/status.ts | 9 + packages/cantos/src/act/stories.ts | 144 +++++++++++++ packages/cantos/src/act/story-kinds.ts | 41 ++++ packages/cantos/src/act/story-options.ts | 27 +++ packages/cantos/src/act/story-types.ts | 16 ++ packages/cantos/src/act/storyteller.ts | 134 ++++++++++++ packages/cantos/src/act/test-kinds.ts | 25 +++ packages/cantos/src/act/utils.ts | 31 +++ packages/cantos/src/commons.ts | 21 ++ packages/cantos/src/consts.ts | 40 ++++ packages/cantos/src/diagram.ts | 15 ++ packages/cantos/src/entrance.ts | 64 ++++++ packages/cantos/src/index.ts | 22 ++ packages/cantos/src/parser/yaml-parser.ts | 14 ++ packages/cantos/src/util-types.ts | 1 + .../tests/fixtures/common-scenes-fixture.ts | 11 + .../tests/fixtures/story-test-fixture.ts | 36 ++++ .../tests/fixtures/wizard-of-oz-story.ts | 132 ++++++++++++ .../tests/fixtures/writer/example.cast.yaml | 3 + .../tests/fixtures/writer/example.story.yaml | 3 + .../cantos/tests/units/common-stories.test.ts | 12 ++ packages/cantos/tests/units/stories.test.ts | 21 ++ .../cantos/tests/units/story-teller.test.ts | 61 ++++++ packages/cantos/tests/utils/setup.ts | 0 packages/cantos/tsconfig.build.json | 9 + packages/cantos/tsconfig.json | 29 +++ packages/cantos/vite.config.ts | 34 +++ 33 files changed, 1369 insertions(+), 1 deletion(-) create mode 100644 packages/cantos/.eslintrc.cjs create mode 100644 packages/cantos/LICENSE.md create mode 100644 packages/cantos/README.md create mode 100644 packages/cantos/package.json create mode 100644 packages/cantos/src/act/interfaces.ts create mode 100644 packages/cantos/src/act/status.ts create mode 100644 packages/cantos/src/act/stories.ts create mode 100644 packages/cantos/src/act/story-kinds.ts create mode 100644 packages/cantos/src/act/story-options.ts create mode 100644 packages/cantos/src/act/story-types.ts create mode 100644 packages/cantos/src/act/storyteller.ts create mode 100644 packages/cantos/src/act/test-kinds.ts create mode 100644 packages/cantos/src/act/utils.ts create mode 100644 packages/cantos/src/commons.ts create mode 100644 packages/cantos/src/consts.ts create mode 100644 packages/cantos/src/diagram.ts create mode 100644 packages/cantos/src/entrance.ts create mode 100644 packages/cantos/src/index.ts create mode 100644 packages/cantos/src/parser/yaml-parser.ts create mode 100644 packages/cantos/src/util-types.ts create mode 100644 packages/cantos/tests/fixtures/common-scenes-fixture.ts create mode 100644 packages/cantos/tests/fixtures/story-test-fixture.ts create mode 100644 packages/cantos/tests/fixtures/wizard-of-oz-story.ts create mode 100644 packages/cantos/tests/fixtures/writer/example.cast.yaml create mode 100644 packages/cantos/tests/fixtures/writer/example.story.yaml create mode 100644 packages/cantos/tests/units/common-stories.test.ts create mode 100644 packages/cantos/tests/units/stories.test.ts create mode 100644 packages/cantos/tests/units/story-teller.test.ts create mode 100644 packages/cantos/tests/utils/setup.ts create mode 100644 packages/cantos/tsconfig.build.json create mode 100644 packages/cantos/tsconfig.json create mode 100644 packages/cantos/vite.config.ts diff --git a/README.md b/README.md index f12ec8d..014c7f0 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,6 @@ Sometimes, this is inevitable as part of the process of development, but sometim ### Helpers ```ts -import {A} from 'test-acts' ``` - :heavy_check_mark: **Plan** your tests in a tree structure. diff --git a/packages/cantos/.eslintrc.cjs b/packages/cantos/.eslintrc.cjs new file mode 100644 index 0000000..986818e --- /dev/null +++ b/packages/cantos/.eslintrc.cjs @@ -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": { + + } +} diff --git a/packages/cantos/LICENSE.md b/packages/cantos/LICENSE.md new file mode 100644 index 0000000..24d8d47 --- /dev/null +++ b/packages/cantos/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2023 Bo An + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/cantos/README.md b/packages/cantos/README.md new file mode 100644 index 0000000..5dab8f7 --- /dev/null +++ b/packages/cantos/README.md @@ -0,0 +1,199 @@ +Work in progress, see [tests](packages/cantos/tests) for working examples +--- + +# Cantos: Write Better Tests + +Cantos is a simple, lightweight, framework-agnostic typescript design library to help you write better design documents +with easy, and support test-driven and/or behavior-driven development. + +## Quick Start + +```ts + + +``` + +## What Cantos does + +### Cantos provides a way to think through your software + +The main purpose of Cantos is to help you write the feature part of your `design document`, e.g. __what__ your software +__should__ do. + +It's an easy and great way to let you reason about your software with natural language before you write the code. + +Cantos prioritizes `user stories` over `technical spec` as the way to write the stories from the user's +perspective. + +Writing `user stories` often gives you a more purposeful and stable design that is less likely to change compared to +implementation details. + +It means you get to think through the __purposes and features__ of your software ___in detail___ without getting bogged +down ___by the details___ of __implementation__ too early. + +It's about thinking in terms of + +- the ___what___, +- the ___interfaces___, +- the ___scenarios___, and +- the ___things to test___, + +before thinking about + +- the _how_, +- the _implementations_, +- the _technical workflows_, and +- the _actual tests_ to write. + +Usually, design document and development management is done using either regular text documents or on platforms such as +Jira, Confluence, etc. + +Cantos offers an option to similar things in your Typescript program and gives you better developing and testing +experience with easy integration with your existing test frameworks. + +### Cantos is a tool to help you write better tests in general + +#### Usual way of writing tests + +Too often we write tests for features in adhoc fashions, if at all: + +1. We first come up with an idea for a feature, +2. we write the code, +3. and then we write some tests (or just "click around") to verify the code, +4. and, to these tests, we give labels such as "this should work", "that should render", or "those should load"--whatever makes sense at the moment. +5. we add more features, and repeat the steps above. + +Even with TDD workflow, we often just reverse the order of the steps above by doing 3 (writing test) and 4 (designing +test) before 2 (coding). + +##### Problems with the usual way of writing tests + +Often, these work OK until our program becomes more complex when we find that, to name a possible few: + +- Initial design of the feature is not well thought through, and as a result both the code and tests need to be re-written. +- Tests are tightly coupled with the implementation details. It means __too many__ tests fails with a slight change of our code, which is the design flaw of our initially test because they were specifically written for the initial codes. +- Test labels need to be kept in sync, e.g. what used to be "this should work" is now "this should work with this and that". +- The tests are not well organized and becomes long suites difficult to navigate. +- The tests descriptions are too technical and hard to understand sometimes without looking at the actual tests. +- It is hard to figure out what a test is _really_ testing and what larger purpose some tests is serving together (for example, to make sure a connection error dialog is shown when the user lost internet connection). + +#### Cantos way of writing tests + +Cantos lets you use your prepared user stories as __the single source of truth for the feature design of your software__ +to be directly used in your tests (unit, integration, e2e, ...) descriptions and other places. + +It effectively turns your tests into + +- clear and readable stories. +- a mirror of the organization of your features designs (rather than implementations). + +You no longer needs to manually keep your tests descriptions and organizations and design documents in sync. No more tedious writing and re-writing of test labels such as "this should work", "that should render", or "those should load." Instead, simply refer to your stories (with IDE autocomplete) to explain what your tests are about. + +##### Improve your testing workflow + +Cantos helps you mitigate the above hypothetical issues by letting you do things differently: + +1. Write story first + +- You first write our your stories (features, scenarios, behaviors, etc.), which allows you to think through the features as a whole. + +```ts + +``` + +2. Write tests based on the stories + +- You then write your tests based on the stories, using the stories as labels for your tests, which explains their larger purpose and relations to features. + - Most often, you only write the __larger tests__ units, such as "user should get warning when the connection is lost". + - Remember, you don't need to write these labels, but directly use the stories as labels which will be automatically synced. + - Under the larger test units, you write __smaller tests__ for the implementation details, such as "Warning dialogue should be disposed of when the user click outside the dialogue". + +3. Develop and refactor with ease + +- You then write your code to make the tests pass. +- When you need to refactor, you + - You update the stories, e.g. change it to say you want your app to be more explicit about available user interactions. + - You update the tests, which has become much easier: + - Navigate to the __larger tests units__ linked to your story which are already updated and remain __unchanged__ + - Then, you drop __the smaller tests__ that are no longer relevant, such as the clicking-outside-warning-dialog test, and + - write new ones, such as a test with the "User should be presented with a button to close the dialog" under the larger test units. + +### Incremental adoption, framework-agnostic, lightweight, and composable + +Cantos is designed to be easily adoptable for your existing JS testing frameworks and runners such as +Jest/Vitest/Playwright/... + +You can simply add it like this to your existing test files: + +```ts +import {C} from 'cantos' + +``` + +- Zero overhead and easy incremental adoption to use with your existing test frameworks and runners such as + Jest/Vitest/Playwright/... +- Lightweight: ~3kb gzipped. + +> BDD is a special flavor of TDD +> +> +> + +## Ideas behind Cantos + +Cantos simply rehashes some of the old ideas of BDD and software engineering principles in general, for example, + +### 1. What before How + +Did you ever feel that you only started to know what your software __reall__ is, after you have written the code? + +Sometimes, this is inevitable as part of the process of development, but sometimes + +```ts +``` + +### 2. Specification before Implementation + +### 3. User before Developer + +- A User story + +| Is | Is Not | +|---------------------------------------|---------------------------------------------| +| From a user's perspective | From a developer's perspective | +| Tells a narrative, i.e. a story | Tells about implementation detail | +| __Concrete__ like stories in contexts | __Concrete__ as a detailed technical issues | +| + +### 4. Scenario before Testing1 + +## Use Test Acts with TDD/BDD workflows + +## Perspective of the users + +### Helpers + +```ts +``` + +- :heavy_check_mark: **Plan** your tests in a tree structure. + +## Examples + +### Use satisfies to get intellisense for UserAct + +```ts + + +``` + +- It works great with CoPilot because it has a clear structure to infer from. + +## Glossary + +GWT +: `Given`-`When`-`Then` + +Step +: A reusable component of in a user `story` consists of `GWT`, usually under a `feature`. The same meaning as `act` but +used in the context of BDD user stories. diff --git a/packages/cantos/package.json b/packages/cantos/package.json new file mode 100644 index 0000000..e5528af --- /dev/null +++ b/packages/cantos/package.json @@ -0,0 +1,50 @@ +{ + "name": "cantos", + "private": false, + "version": "0.0.1", + "type": "module", + "main": "./dist/cantos.umd.cjs", + "module": "./dist/cantos.js", + "types": "./dist/cantos.d.ts", + "exports": { + ".": { + "import": "./dist/cantos.js", + "require": "./dist/cantos.umd.cjs" + } + }, + "files": [ + "dist" + ], + "scripts": { + "dev": "vite", + "build": "tsc --project tsconfig.build.json && vite build", + "preview": "vite preview", + "test": "vitest", + "test:ui": "vitest --ui", + "mermaid": "^10.6.1" + }, + "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", + "vite": "^4.4.5", + "vite-plugin-dts": "^3.6.3", + "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" +} diff --git a/packages/cantos/src/act/interfaces.ts b/packages/cantos/src/act/interfaces.ts new file mode 100644 index 0000000..8e75e1d --- /dev/null +++ b/packages/cantos/src/act/interfaces.ts @@ -0,0 +1,106 @@ +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[]; + +} + +export interface StoryActor { + role?: string; + roleBio?: string; + /** + * The name of the role. + */ + actor?: string; + actorBio?: string; +} + +export const EmptyCast = {} as const; +type KeysOfConst = T extends Record ? keyof T : never; +export type CastProfiles = Record; + +/** + * Bare Act is the base structure for any entity or behavior without methods or nested entities. + */ +export interface IStoryScript { + scenes?: IStoryScripts; + /** + * The order of the story among its sibling stories. + */ + order?: number; + cast?: Cast; + /** + * The short version of the story. + */ + story: string; + /** + * Story-by-Story options that can override the global story options. + */ + options?: StoryOptions; + genre?: Genres; + tests?: Record + 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[]; + + who?: KeysOfConst[]; + + + 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; +} diff --git a/packages/cantos/src/act/status.ts b/packages/cantos/src/act/status.ts new file mode 100644 index 0000000..b215578 --- /dev/null +++ b/packages/cantos/src/act/status.ts @@ -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', +} diff --git a/packages/cantos/src/act/stories.ts b/packages/cantos/src/act/stories.ts new file mode 100644 index 0000000..3b12ee7 --- /dev/null +++ b/packages/cantos/src/act/stories.ts @@ -0,0 +1,144 @@ +import {PartialDeep, ReadonlyDeep} from "type-fest"; +import {ACT_DEFAULT_DESCRIPTIONS} from "@src/consts.ts"; +import {Scenes} from "@src/entrance.ts"; + +import {CastProfiles, EmptyCast, 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, + 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, + 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, CAST extends CastProfiles > = ReadonlyDeep & Story & { + scenes: { [K in keyof T['scenes']]: T['scenes'][K] extends IStoryScript ? ReadonlyDeep> : never }; +}; + +export type UserCast = ReadonlyDeep; + +export type UserStories, CAST extends CastProfiles> = ReadonlyDeep<{ [K in keyof T]: ReadonlyDeep> }>; + +export type UserNameList = ReadonlyDeep<{ + [K in keyof T]: ReadonlyDeep> +}>; + +export {TestKind}; + diff --git a/packages/cantos/src/act/story-kinds.ts b/packages/cantos/src/act/story-kinds.ts new file mode 100644 index 0000000..cbce61b --- /dev/null +++ b/packages/cantos/src/act/story-kinds.ts @@ -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; diff --git a/packages/cantos/src/act/story-options.ts b/packages/cantos/src/act/story-options.ts new file mode 100644 index 0000000..f289cd3 --- /dev/null +++ b/packages/cantos/src/act/story-options.ts @@ -0,0 +1,27 @@ +import {Genres} from "@src/act/story-kinds.ts"; + +export enum StoryVersion { + NO_PREFERENCE = "NO_PREFERENCE", + SHORT = "SHORT", + LONG = "LONG", +} + +export enum StoryTag { + Genre = "Genre", + Status = "Status", + Priority = "Priority", +} + +export interface StoryTellingOptions { + /** + * Whether to tell the story in short or long version when unspecified, i.e. when calling `tell()` + */ + prefer?: StoryVersion + tags?: StoryTag[] +} + +export interface StoryOptions { + defaultKind?: Genres + capitalizeKeywords?: boolean + teller?: StoryTellingOptions +} diff --git a/packages/cantos/src/act/story-types.ts b/packages/cantos/src/act/story-types.ts new file mode 100644 index 0000000..ddb5d4e --- /dev/null +++ b/packages/cantos/src/act/story-types.ts @@ -0,0 +1,16 @@ +import {CastProfiles, EmptyCast, IStoryScript} from "@src/act/interfaces.ts"; + + +/** + * Export type alises for easier use for user-defined data. + */ +type IStoryScripts = Record>; + +/** + * Type for used defined input data for an Act. + */ +type StoryScript = IStoryScript; + + +export type {StoryScript, IStoryScripts} + diff --git a/packages/cantos/src/act/storyteller.ts b/packages/cantos/src/act/storyteller.ts new file mode 100644 index 0000000..e89751d --- /dev/null +++ b/packages/cantos/src/act/storyteller.ts @@ -0,0 +1,134 @@ +import {StoryScript} from "@src/index.ts"; +import {ACT_DEFAULT_DESCRIPTIONS, GWT_DESCRIPTIONS, STORY_TELLER} from "@src/consts.ts"; +import {Story} from "@src/act/stories.ts"; +import {StoryTag, StoryVersion} from "@src/act/story-options.ts"; +import {Test} from "@src/act/interfaces.ts"; + +enum STATEMENT_TYPE { + GIVEN, + WHEN, + THEN, +} + +export const printTag = (tagText: string) => STORY_TELLER.LEFT_BRACKET + tagText.toUpperCase() + STORY_TELLER.RIGHT_BRACKET; + +export const printTags = (tags: string[]) => tags.map(tag => printTag(tag)).join(STORY_TELLER.SPACE); + +export const printTestTags = (tests: Test[]) => { + const tags = tests.map(test => test.kind); + return tags.length > 0 ? printTags(tags) : ''; +} + +function gatherTags(story: Story, tags: StoryTag[]): string { + const allTags: string [] = []; + + for (const tag of tags) { + let tagText = ''; + switch (tag) { + case StoryTag.Genre: + tagText = story.genre ? story.genre : ''; + break; + case StoryTag.Status: + tagText = story.status ? story.status : ''; + break; + case StoryTag.Priority: + tagText = story.priority ? story.priority.toString() : ''; + break; + } + + const tagTextTrimmed = tagText.trim(); + + if (tagTextTrimmed.length > 0) { + allTags.push(tagTextTrimmed); + + } + + } + + return allTags.length > 0 ? printTags(allTags) : ''; +} + +export function tellStory(story: Story, version: StoryVersion): string { + const opt = story.options; + let storyText = ''; + switch (version) { + case StoryVersion.SHORT: + storyText = story.story; + break; + case StoryVersion.LONG: + storyText = gatherAllActsStatements(story); + break; + // if no preference specified, use the option preference or default to short + case StoryVersion.NO_PREFERENCE: + default: + storyText = opt?.teller?.prefer === StoryVersion.LONG ? gatherAllActsStatements(story) : story.story; + break; + } + if (opt?.teller?.tags) { + storyText = gatherTags(story, opt?.teller?.tags) + STORY_TELLER.SPACE + storyText; + } + return storyText; + + +} + +export function getActStatements(acts: StoryScript[] | undefined, prefixType: STATEMENT_TYPE, actName?: string): string | undefined { + if (!acts) { + return undefined; + } + + const actsLength = acts.length; + if (actsLength === 0) { + return undefined; + } + + let prefix = ""; + switch (prefixType) { + case STATEMENT_TYPE.GIVEN: + prefix = GWT_DESCRIPTIONS.GIVEN + break; + case STATEMENT_TYPE.WHEN: + prefix = GWT_DESCRIPTIONS.WHEN + break; + case STATEMENT_TYPE.THEN: + prefix = GWT_DESCRIPTIONS.THEN + break; + default: + break; + } + + const statement = acts.map((given, index) => { + let statement = given.story.trim(); + // there are more than one given + if (actsLength > 1) { + // if it's not the first given, add a comma + statement = index === 0 ? statement : `, ${statement}`; + // if it's the second last given, add an "and" after the comma + statement = index === actsLength - 2 ? `${statement} and` : statement; + // if it's the last given, add a period after the comma + statement = index === actsLength - 1 ? `${statement}.` : statement; + } + return statement.trim(); + }).join(); + + return `${prefix.toLowerCase()} ${actName ? actName + ' ' : ''}${statement}`; +} + +export function gatherAllActsStatements(entity: Story): string { + const actName = entity.protagonist ?? ACT_DEFAULT_DESCRIPTIONS.DEFAULT_ACT_NAME; + const statements = [ + getActStatements(entity.context, STATEMENT_TYPE.GIVEN), + getActStatements(entity.when, STATEMENT_TYPE.WHEN), + getActStatements(entity.then, STATEMENT_TYPE.THEN, actName), + ] + + const collectedStatements = statements.filter(statement => statement).join(", ").trim(); + // if no collect statement or the statement is empty, fallback to description + if (!collectedStatements || collectedStatements === "") { + return entity.story; + } else { + // return with first letter capitalized + return collectedStatements.charAt(0).toUpperCase() + collectedStatements.slice(1); + } + +} diff --git a/packages/cantos/src/act/test-kinds.ts b/packages/cantos/src/act/test-kinds.ts new file mode 100644 index 0000000..bdaae27 --- /dev/null +++ b/packages/cantos/src/act/test-kinds.ts @@ -0,0 +1,25 @@ +export enum TestKinds { + /** + * Quick and dirty way to determine whether something works at all. + */ + SMOKE = 'SMOKE', + /** + * Default kind for an Act. + */ + CORNER_CASE = 'CORNER', + UNIT = 'UNIT', + INTEGRATION = 'INTEGRATE', + END_TO_END = 'E2E', + PERFORMANCE = 'PERF', + REGRESSION = 'REGRESS', + SECURITY = 'SECURITY', + STRESS = 'STRESS', + ACCEPTANCE = 'ACCEPTANCE', +} + +export const TestKind = { + ...TestKinds, +} + +export type TestKind = TestKinds; + diff --git a/packages/cantos/src/act/utils.ts b/packages/cantos/src/act/utils.ts new file mode 100644 index 0000000..97de794 --- /dev/null +++ b/packages/cantos/src/act/utils.ts @@ -0,0 +1,31 @@ +import {IStory} from "@src/act/interfaces.ts"; + +export function populatePath>(entity: T, parentPath?: string): T { + // Add name to parentPath if any + return { + ...entity, + path: getPath(entity.story, parentPath), + } +} + + +export function populateActPath(actor: IStory): IStory { + const actorPath = actor.story; + // populate entity acts + for (const actKey in actor.scenes) { + actor.scenes[actKey] = populatePath(actor.scenes[actKey], actorPath); + } + return populatePath(actor); +} + + +/** + * Util classes + * @param entityDescribe + * @param parentPath + */ + +export function getPath(entityDescribe: string, parentPath: string | undefined): string { + return parentPath ? `${parentPath}.${entityDescribe}` : entityDescribe; +} + diff --git a/packages/cantos/src/commons.ts b/packages/cantos/src/commons.ts new file mode 100644 index 0000000..bcc1ae3 --- /dev/null +++ b/packages/cantos/src/commons.ts @@ -0,0 +1,21 @@ +import {COMMON_TEST_DESCRIPTIONS, COMMON_TEST_TAGS} from "@src/consts.ts"; +import {IStoryScripts} from "@src/act/story-types.ts"; +import {loadList} from "@src/entrance.ts"; + +/** + * + */ +export const CommonScences = loadList(COMMON_TEST_DESCRIPTIONS); + +export const CommonTest = loadList(COMMON_TEST_TAGS) + +export const customizeCommonTests = >(behaviors: T) => { + return { + ...CommonScences, + ...behaviors, + } +} + + + + diff --git a/packages/cantos/src/consts.ts b/packages/cantos/src/consts.ts new file mode 100644 index 0000000..57a8f8b --- /dev/null +++ b/packages/cantos/src/consts.ts @@ -0,0 +1,40 @@ +import {TestKinds} from "@src/act/test-kinds.ts"; + +export const ACT_DEFAULT_DESCRIPTIONS = { + DEFAULT_NARRATIVE: "The story goes", + DEFAULT_ACT_NAME: "it", +} + +export const STORY_TELLER = { + LEFT_BRACKET: "[", + RIGHT_BRACKET: "]", + SPACE : " ", +} + +export const GWT_DESCRIPTIONS = { + GIVEN: "Given that", + WHEN: "When", + THEN: "Then", +} + +export const COMMON_TEST_DESCRIPTIONS = { + SHOULD_WORK: "should work", + SHOULD_FAIL: "should fail", + SHOULD_BE_TRUTHY: "should be truthy", + SHOULD_BE_FALSY: "should be falsy", + SHOULD_RENDER: "should render", + SHOULD_NOT_RENDER: "should not render", + SHOULD_PROVIDE_RESULT: "should provide result", + SHOULD_NOT_PROVIDE_RESULT: "should not provide result", +} +export const COMMON_TEST_TAGS = { + INTEGRATION: TestKinds.INTEGRATION, + UNIT: TestKinds.UNIT, + SMOKE: TestKinds.SMOKE, + END_TO_END: TestKinds.END_TO_END, + PERFORMANCE: TestKinds.PERFORMANCE, + REGRESSION: TestKinds.REGRESSION, + SECURITY: TestKinds.SECURITY, + STRESS: TestKinds.STRESS, + +} diff --git a/packages/cantos/src/diagram.ts b/packages/cantos/src/diagram.ts new file mode 100644 index 0000000..892bbe7 --- /dev/null +++ b/packages/cantos/src/diagram.ts @@ -0,0 +1,15 @@ +import { Story } from "./act/stories"; + +export const drawEntity = (entity: Story) : string => { + let mermaidCode = `graph LR\n`; + mermaidCode += ` ${entity.story.replace(/\s+/g, '_')}["${entity.story}"]\n`; + + for (const actKey in entity.scenes) { + if (Object.prototype.hasOwnProperty.call(entity.scenes, actKey)) { + const act = entity.scenes[actKey]; + mermaidCode += ` ${entity.story.replace(/\s+/g, '_')} --> ${actKey.replace(/\s+/g, '_')}["${act.story}"]\n`; + } + } + + return mermaidCode; +} diff --git a/packages/cantos/src/entrance.ts b/packages/cantos/src/entrance.ts new file mode 100644 index 0000000..d2c72ee --- /dev/null +++ b/packages/cantos/src/entrance.ts @@ -0,0 +1,64 @@ +// Utility function to map descriptions to act records +// Utility function to map descriptions to act records +import {NameList} from "@src/util-types.ts"; +import {Story, UserNameList, UserStories, UserStory} from "@src/act/stories.ts"; + +import {CastProfiles, IStory, IStoryScript} from "@src/act/interfaces.ts"; +import {IStoryScripts} from "@src/act/story-types.ts"; +import {StoryOptions} from "@src/act/story-options.ts"; + +type StoryField = { story: string } + +function mapNameList(tell: T): Record { + return Object.keys(tell).reduce((acc, key) => { + acc[key as keyof T] = {story: tell[key as keyof T]}; + return acc; + }, {} as Record); +} + +export function loadList(descriptions: T): UserNameList { + const result = mapNameList(descriptions); + return loadScriptRecord(result) as UserNameList; +} + +/** + * This function creates an Act from a PartialAct. + * @param partialEntity - The PartialAct to be converted into an Act. + * @param opt - Optional parameters. + * @returns A ReadonlyDeep Act. + */ +export function loadScript