{"id":13847424,"url":"https://github.com/h-o-t/entente","last_synced_at":"2025-10-30T08:23:27.068Z","repository":{"id":42813803,"uuid":"268938701","full_name":"h-o-t/entente","owner":"h-o-t","description":"A convention testing framework for JavaScript/TypeScript.","archived":false,"fork":false,"pushed_at":"2023-01-07T21:04:11.000Z","size":529,"stargazers_count":22,"open_issues_count":8,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-08-05T18:23:32.978Z","etag":null,"topics":["conventions","javascript","testing","typescript"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/h-o-t.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-06-03T00:36:06.000Z","updated_at":"2023-09-08T18:08:19.000Z","dependencies_parsed_at":"2023-02-08T01:45:47.266Z","dependency_job_id":null,"html_url":"https://github.com/h-o-t/entente","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/h-o-t%2Fentente","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/h-o-t%2Fentente/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/h-o-t%2Fentente/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/h-o-t%2Fentente/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/h-o-t","download_url":"https://codeload.github.com/h-o-t/entente/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":225812898,"owners_count":17528083,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["conventions","javascript","testing","typescript"],"created_at":"2024-08-04T18:01:19.805Z","updated_at":"2025-10-30T08:23:26.998Z","avatar_url":"https://github.com/h-o-t.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"# entente\n\n![CI](https://github.com/h-o-t/entente/workflows/CI/badge.svg)\n[![npm version](https://img.shields.io/npm/v/entente)](https://www.npmjs.com/package/entente)\n\nA convention testing framework for JavaScript/TypeScript.\n\n**This project is heavily under development and APIs are far from stable. Use at\nyour own risk.**\n\n## What is convention testing?\n\nConvention testing allows you to enforce type and style conventions in your\ncode base. Think of it as an advanced linter which you author specific tests\nfor conventions you want in your code base.\n\nFor example, let's say you have a convention that each module you create in\nyour project has a named export of a function named `render`, that takes a\nsingle options argument and returns an object.\n\nAn example module's code might look like this:\n\n```js\nimport h from \"h\";\n\nexport function render({ state }) {\n  return h(\"h1\", {}, state.title);\n}\n```\n\nAnd you want to make sure all the views in your project follow this pattern. You\ncould enforce this as a convention test like this:\n\n```js\nimport { assertSourceFile, createProject, test } from \"entente\";\n\nconst project = createProject(\"../src/index.js\");\n\nconst views = project\n  .getSourceFiles()\n  .filter((sf) =\u003e sf.getFilePath().match(/View\\.js$/));\n\nfor (const view in views) {\n  test({\n    name: `view conventions - ${view.getFilePath()}`,\n    fn() {\n      const renderFn = assertSourceFile(view)\n        .exports.namedExport(\"render\")\n        .length(1)\n        .declarations[0].isFunctionLike();\n      renderFn.parameters.length(1).paramater(0).isObject().isNotOptional();\n      renderFn.return.isObject();\n    },\n  });\n}\n```\n\nIf any of the assertions made in the test were not true for any files that were\nnamed liked `*View.js` in your project, then an assertion error would be thrown\nalerting you to the conventions not being followed in your code.\n\nAll forms of testing have their limits, including convention testing. Most\nother forms of testing check inputs and outputs and don't actually introspect\nthe code itself to see how the work is being accomplished.\n\nThis presents a challenge of scaling development on a code base, where you\nreally want to ensure code not only provides the behaviour expected but also\nconforms to the patterns you have established. Convention testing solves this\nproblem.\n\n## How does it work?\n\nIn order to make assertions against the code as it is written, we need the\nability to introspect the code. Entente leverages\n[ts-morph](https://ts-morph.com), which in turn leverages the\n[TypeScript](https://www.typescriptlang.org/) compiler. The TypeScript compiler\ncan take JavaScript and TypeScript code and transform it into a rich abstract\nsyntax tree (AST), which ts-morph provides a more usable interface to work with\nthe generated AST. Entente then provides a simple test harness and an assertion\ninterface to make it easy to test the conventions in the code.\n\n## Harness\n\nEntente provides a light-weight test harness with two functions. `test()` to\nadd a test to the test queue, and `run()` which will execute any queued tests\nand resolves with the results of the test run.\n\nYou do not have to use the test harness provided with Entente. The assertion\nAPI that is included will throw when assertions are not met, meaning that any\nsort of test harness that captures thrown errors should work fine as a test\nharness for Entente.\n\n### test()\n\nPut at test into the queue.\n\n```ts\ntest(spec: TestFn | TestSpec): void;\n```\n\nThe function accepts either a function or an object with the properties `name`\nand `fn`. When just a function is passed, the name of the function will be used\nas the name of the test, or auto-generated if the function is anonymous. When\nthe return from a test function is promise-like it will wait to be resolved\nbefore executing the next test in the queue. If the function throws or rejects,\nthe test will be marked as a failure.\n\n### run()\n\nExecute any queued tests and return a summary of the results.\n\n```ts\nrun(options?: RunOptions): Promise\u003cRunResult\u003e;\n```\n\nThe function optionally accepts a set of options, where the only currently\nsupported option is `silent`, which defaults to `false`. If `silent` is `true`,\n`run()` will not log anything itself to the console during the test run.\n\nOnce all the queued tests have been run. The function resolves with an array\nwhich contains a result for each test.\n\n## Using with Jest\n\nIn order to utilise convention tests with Jest (and most other test harnesses)\nyou do not need to utilise the included test harness/runner. Only the project\ncreation and assertion APIs need to be included. For example to test that\nevery source file has a default export, you would do something like this:\n\n```ts\nimport { assertSourceFile, createProject } from \"entente\";\n\ndescribe(\"source file has default export\", () =\u003e {\n  const project = createProject(\"./src/index.js\");\n  const sourceFiles = project.getSourceFiles();\n  for (const sourceFile of sourceFiles) {\n    it(`for: ${sourceFile.getFilePath()}`, () =\u003e {\n      const fn = assertSourceFile(sourceFile).exports.default(\n        \"the module has a default export\"\n      );\n    });\n  }\n});\n```\n\n## Projects\n\nIn order to test your code, you first need to get your code parsed and\ntransformed into an AST which can be introspected. Entente supports both\nJavaScript and TypeScript.\n\nTo create a project, you need to import the `createProject()` function:\n\n```js\nimport { createProject } from \"entente\";\n\nconst project = createProject(\"../src/index.js\");\n```\n\n`createProject()` takes a single argument which is the root file of a project,\nbe it JavaScript or TypeScript. If the project is a TypeScript project (or a\nmixed JavaScript/TypeScript project), then you can use the `tsconfig.json` file:\n\n```ts\nconst project = createProject(\"../tsconfig.json\");\n```\n\n`createProject()` returns a ts-morph `Project`, which allows you to get access\nto the AST of the source files associated with the project.\n\n### Source files\n\nA source file is the AST derived from a single input file. Usually each source\nfile is a module, but this depends upon the code you are analysing. To make\nassertions against the code, you likely need to gain access to specific\nsource files. Information on accessing source files from a project is\n[documented here](https://ts-morph.com/navigation/getting-source-files).\n\n## Assertions\n\nIn order to test your conventions, you will want to make assertions against the\nAST, and Entente provides several interfaces that allow you to make those\nassertions.\n\nThe general concept is that you pass an assertion function a particular type\nof AST node, which returns an interface which makes assertions against that node\nand provides properties which allow you to \"dig\" into the node.\n\nThe general pattern for the assertions is that properties \"navigate\" into the\nAST returning a specific interface to make assertions against that value, where\nas methods actually make an assertion against the node, and will will throw if\nthe assertion isn't true. If the method doesn't throw, it might return the\nsame interface again, or it may delve \"deeper\" into the AST, depending on what\nmakes the most sense.\n\nA general workflow would be to obtain a reference to a source file and pass that\nas an argument to `assertSourceFile()` which would provide an interface where\nyou can start to make additional assertions. For example:\n\n```js\nimport { createProject, assertSourceFile, test, run } from \"entente\";\n\nconst project = createProject(\"../src/index.js\");\n\nfor (const sf of project.getSourceFiles()) {\n  test({\n    name: `has default export - ${sf.getFilePath()}`,\n    fn() {\n      assertSourceFile(sf).exports.default(\"Expected a default export.\");\n    },\n  });\n}\n\nrun();\n```\n\nThis would check that every module in our project has a default export. If any\nof them did not, then the test would fail and an error would be logged to the\nconsole.\n\n## Using JavaScript\n\nEntente can determine quite a lot about JavaScript code just from its usage, but\nin many cases, certain things might not be statically determinable. Because\nEntente uses the TypeScript compiler to get this analysis, the richer the\ninformation provided to the TypeScript compiler, the richer the information that\nis available for making assertions and therefore testing conventions.\n\nTypeScript supports a lot of\n[JSDoc](https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html#supported-jsdoc)\nto enrich the understanding of the code. Adding supported JSDoc to your\nJavaScript code will also seemlessly increase the amount of information\navailable when testing.\n\n---\n\nLicense MIT.\n\nCopyright 2020 Kitson P. Kelly. All rights reserved.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fh-o-t%2Fentente","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fh-o-t%2Fentente","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fh-o-t%2Fentente/lists"}