├── .editorconfig ├── .github ├── actions │ └── setup │ │ └── action.yml ├── renovate.json5 └── workflows │ ├── ci.yml │ ├── docs.yml │ ├── plan-release.yml │ ├── pr.yml │ └── publish.yml ├── .gitignore ├── .node-version ├── .npmrc ├── .release-plan.json ├── .vscode └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── RELEASE.md ├── docs ├── .vitepress │ └── config.mjs ├── actions.md ├── command.md ├── composing.md ├── functions.md ├── getting-started.md ├── index.md ├── installation.md ├── links.md ├── super-rentals-recommendations.md ├── testing.md ├── ui.md ├── usage.md └── why.md ├── ember-command ├── .eslintignore ├── .eslintrc.cjs ├── .gitignore ├── .prettierignore ├── .prettierrc.cjs ├── .template-lintrc.js ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── addon-main.cjs ├── babel.config.mjs ├── package.json ├── rollup.config.mjs ├── src │ ├── -private │ │ ├── -owner.ts │ │ ├── action.ts │ │ ├── command.ts │ │ ├── decorator.ts │ │ ├── instance.ts │ │ └── link-command.ts │ ├── components │ │ └── command-element.gts │ ├── helpers │ │ └── command.ts │ ├── index.ts │ ├── template-registry.ts │ └── test-support │ │ └── index.ts ├── tsconfig.json ├── tsconfig.typedoc.json ├── typedoc.json └── types │ ├── ember-element-helper │ └── helpers │ │ └── element.d.ts │ └── index.d.ts ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── test-app ├── .ember-cli ├── .eslintignore ├── .eslintrc.cjs ├── .gitignore ├── .prettierignore ├── .prettierrc.cjs ├── .template-lintrc.js ├── .watchmanconfig ├── app ├── actions │ └── mutate-action.ts ├── app.ts ├── components │ ├── button.gts │ ├── command-demo │ │ ├── component.ts │ │ ├── counter-decrement-command.ts │ │ ├── counter-increment-command.ts │ │ ├── foobar-log-command.ts │ │ ├── push-log-command.ts │ │ └── template.hbs │ └── outer-demo │ │ ├── component.ts │ │ ├── cook-curry-command.ts │ │ └── template.hbs ├── config │ └── environment.d.ts ├── index.html ├── router.ts ├── routes │ └── .gitkeep ├── services │ └── counter.ts ├── styles │ └── app.css └── templates │ ├── application.hbs │ ├── route-a.hbs │ ├── route-b.hbs │ └── route-c.hbs ├── config ├── ember-cli-update.json ├── ember-try.js ├── environment.js ├── optional-features.json └── targets.js ├── ember-cli-build.js ├── package.json ├── public └── robots.txt ├── testem.js ├── tests ├── -owner.ts ├── commands │ └── task-command.ts ├── index.html ├── integration │ ├── commands │ │ ├── counter-test.ts │ │ ├── foobar-test.ts │ │ └── task-test.ts │ └── helpers │ │ └── command-test.ts ├── rendering │ ├── action-test.gts │ ├── command-element-test.ts │ └── mutation-test.gts ├── test-helper.ts └── unit │ ├── action-test.ts │ ├── command-decorator-test.ts │ └── command-test.ts ├── tsconfig.json └── types ├── ember-sinon-qunit.d.ts └── index.d.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.hbs] 16 | insert_final_newline = false 17 | 18 | [*.{diff,md}] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: "Setup" 2 | # inputs: 3 | # who-to-greet: # id of input 4 | # description: 'Who to greet' 5 | # required: true 6 | # default: 'World' 7 | runs: 8 | using: "composite" 9 | steps: 10 | - name: Install pnpm 11 | uses: pnpm/action-setup@v4 12 | 13 | - name: Use Node.js 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version-file: ".node-version" 17 | cache: "pnpm" 18 | # This creates an .npmrc that reads the NODE_AUTH_TOKEN environment variable 19 | # necessary for publish 20 | registry-url: "https://registry.npmjs.org" 21 | 22 | - name: Install dependencies 23 | run: pnpm install 24 | shell: bash 25 | 26 | - name: Build package 27 | run: pnpm build 28 | shell: bash 29 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>gossi/frontend-configs//renovate/manage.json5", 5 | "github>gossi/frontend-configs//renovate/labels.json5", 6 | "github>gossi/frontend-configs//renovate/packages.json5", 7 | "github>gossi/frontend-configs//renovate/frontend-configs.json5" 8 | ] 9 | } -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | push: 7 | # filtering branches here prevents duplicate builds from pull_request and push 8 | branches: 9 | - main 10 | - "v*" 11 | # always run CI for tags 12 | tags: 13 | - "*" 14 | 15 | # early issue detection: run CI weekly on Sundays 16 | schedule: 17 | - cron: "0 6 * * 0" 18 | 19 | env: 20 | CI: true 21 | 22 | jobs: 23 | build-docs: 24 | name: Build Docs 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v4 28 | - name: Setup 29 | uses: ./.github/actions/setup 30 | - name: Build Docs 31 | run: pnpm build:docs 32 | 33 | lint-js: 34 | name: lint:js 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v4 38 | - name: Setup 39 | uses: ./.github/actions/setup 40 | - run: pnpm run -r --parallel --aggregate-output lint:js 41 | 42 | lint-hbs: 43 | name: lint:hbs 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v4 47 | - name: Setup 48 | uses: ./.github/actions/setup 49 | - run: pnpm run -r --parallel --aggregate-output lint:hbs 50 | 51 | lint-types: 52 | name: lint:types 53 | runs-on: ubuntu-latest 54 | steps: 55 | - uses: actions/checkout@v4 56 | - name: Setup 57 | uses: ./.github/actions/setup 58 | - run: pnpm run -r --parallel --aggregate-output lint:types 59 | 60 | test: 61 | name: Test 62 | runs-on: ubuntu-latest 63 | steps: 64 | - uses: actions/checkout@v4 65 | - name: Setup 66 | uses: ./.github/actions/setup 67 | - name: Test 68 | run: pnpm run -r --parallel --aggregate-output test 69 | 70 | test-ember-link: 71 | name: Ember Link 72 | runs-on: ubuntu-latest 73 | needs: [test] 74 | 75 | strategy: 76 | matrix: 77 | scenario: 78 | - ember-link-v1 79 | - ember-link-v2 80 | steps: 81 | - uses: actions/checkout@v4 82 | - name: Setup 83 | uses: ./.github/actions/setup 84 | - name: Tests 85 | run: pnpm run -r --parallel --aggregate-output test 86 | - name: Try Scenario 87 | run: pnpm exec ember try:one ${{ matrix.scenario }} --skip-cleanup 88 | working-directory: test-app 89 | 90 | test-try: 91 | name: Ember Try 92 | runs-on: ubuntu-latest 93 | needs: [test] 94 | 95 | strategy: 96 | fail-fast: false 97 | matrix: 98 | try-scenario: 99 | - ember-3.28 100 | - ember-4.4 101 | - ember-4.8 102 | - ember-4.12 103 | - ember-release 104 | - ember-beta 105 | - ember-canary 106 | - embroider-safe 107 | - embroider-optimized 108 | steps: 109 | - uses: actions/checkout@v4 110 | - name: Setup 111 | uses: ./.github/actions/setup 112 | - name: Try Scenario 113 | run: pnpm exec ember try:one ${{ matrix.try-scenario }} --skip-cleanup 114 | working-directory: test-app 115 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | 9 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 10 | permissions: 11 | contents: read 12 | pages: write 13 | id-token: write 14 | 15 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 16 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 17 | concurrency: 18 | group: "pages" 19 | cancel-in-progress: false 20 | 21 | jobs: 22 | build: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - name: Setup 27 | uses: ./.github/actions/setup 28 | - name: Build Docs 29 | run: pnpm build:docs 30 | - name: Setup Pages 31 | uses: actions/configure-pages@v5 32 | - name: Upload artifact 33 | uses: actions/upload-pages-artifact@v3 34 | with: 35 | path: "docs/.vitepress/dist" 36 | 37 | deploy: 38 | environment: 39 | name: github-pages 40 | url: ${{ steps.deployment.outputs.page_url }} 41 | runs-on: ubuntu-latest 42 | needs: build 43 | steps: 44 | - name: Deploy to GitHub Pages 45 | id: deployment 46 | uses: actions/deploy-pages@v4 47 | -------------------------------------------------------------------------------- /.github/workflows/plan-release.yml: -------------------------------------------------------------------------------- 1 | name: Release Plan Review 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - master 7 | pull_request_target: # This workflow has permissions on the repo, do NOT run code from PRs in this workflow. See https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ 8 | types: 9 | - labeled 10 | - unlabeled 11 | 12 | concurrency: 13 | group: plan-release # only the latest one of these should ever be running 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | check-plan: 18 | name: "Check Release Plan" 19 | runs-on: ubuntu-latest 20 | outputs: 21 | command: ${{ steps.check-release.outputs.command }} 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | ref: "main" 28 | # This will only cause the `check-plan` job to have a "command" of `release` 29 | # when the .release-plan.json file was changed on the last commit. 30 | - id: check-release 31 | run: if git diff --name-only HEAD HEAD~1 | grep -w -q ".release-plan.json"; then echo "command=release"; fi >> $GITHUB_OUTPUT 32 | 33 | prepare_release_notes: 34 | name: Prepare Release Notes 35 | runs-on: ubuntu-latest 36 | timeout-minutes: 5 37 | needs: check-plan 38 | permissions: 39 | contents: write 40 | issues: read 41 | pull-requests: write 42 | outputs: 43 | explanation: ${{ steps.explanation.outputs.text }} 44 | # only run on push event if plan wasn't updated (don't create a release plan when we're releasing) 45 | # only run on labeled event if the PR has already been merged 46 | if: (github.event_name == 'push' && needs.check-plan.outputs.command != 'release') || (github.event_name == 'pull_request_target' && github.event.pull_request.merged == true) 47 | 48 | steps: 49 | - uses: actions/checkout@v4 50 | # We need to download lots of history so that 51 | # github-changelog can discover what's changed since the last release 52 | with: 53 | fetch-depth: 0 54 | ref: "main" 55 | - name: Setup 56 | uses: ./.github/actions/setup 57 | - name: "Generate Explanation and Prep Changelogs" 58 | id: explanation 59 | run: | 60 | set +e 61 | pnpm release-plan prepare 2> >(tee -a release-plan-stderr.txt >&2) 62 | 63 | if [ $? -ne 0 ]; then 64 | echo 'text<> $GITHUB_OUTPUT 65 | cat release-plan-stderr.txt >> $GITHUB_OUTPUT 66 | echo 'EOF' >> $GITHUB_OUTPUT 67 | else 68 | echo 'text<> $GITHUB_OUTPUT 69 | jq .description .release-plan.json -r >> $GITHUB_OUTPUT 70 | echo 'EOF' >> $GITHUB_OUTPUT 71 | rm release-plan-stderr.txt 72 | fi 73 | env: 74 | GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }} 75 | 76 | - uses: peter-evans/create-pull-request@v7 77 | with: 78 | commit-message: "Prepare Release using 'release-plan'" 79 | labels: "internal" 80 | branch: release-preview 81 | title: Prepare Release 82 | body: | 83 | This PR is a preview of the release that [release-plan](https://github.com/embroider-build/release-plan) has prepared. To release you should just merge this PR 👍 84 | 85 | ----------------------------------------- 86 | 87 | ${{ steps.explanation.outputs.text }} 88 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: "PR" 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened, labeled, unlabeled, synchronize] 6 | 7 | jobs: 8 | labels: 9 | name: Labels 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checked for required labels 13 | uses: mheap/github-action-required-labels@v5 14 | with: 15 | mode: minimum 16 | count: 1 17 | labels: | 18 | breaking 19 | enhancement 20 | bug 21 | documentation 22 | internal 23 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # For every push to the master branch, this checks if the release-plan was 2 | # updated and if it was it will publish stable npm packages based on the 3 | # release plan 4 | 5 | name: Publish Stable 6 | 7 | on: 8 | workflow_dispatch: 9 | push: 10 | branches: 11 | - main 12 | - master 13 | 14 | concurrency: 15 | group: publish-${{ github.head_ref || github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | check-plan: 20 | name: "Check Release Plan" 21 | runs-on: ubuntu-latest 22 | outputs: 23 | command: ${{ steps.check-release.outputs.command }} 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 0 29 | ref: "main" 30 | # This will only cause the `check-plan` job to have a result of `success` 31 | # when the .release-plan.json file was changed on the last commit. This 32 | # plus the fact that this action only runs on main will be enough of a guard 33 | - id: check-release 34 | run: if git diff --name-only HEAD HEAD~1 | grep -w -q ".release-plan.json"; then echo "command=release"; fi >> $GITHUB_OUTPUT 35 | 36 | publish: 37 | name: "NPM Publish" 38 | runs-on: ubuntu-latest 39 | needs: check-plan 40 | if: needs.check-plan.outputs.command == 'release' 41 | permissions: 42 | contents: write 43 | pull-requests: write 44 | 45 | steps: 46 | - uses: actions/checkout@v4 47 | - name: Setup 48 | uses: ./.github/actions/setup 49 | - name: npm publish 50 | run: pnpm release-plan publish 51 | env: 52 | GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }} 53 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist/ 5 | /tmp/ 6 | declarations/ 7 | 8 | # dependencies 9 | /bower_components/ 10 | /node_modules/ 11 | 12 | # misc 13 | /.env* 14 | /.pnp* 15 | /.sass-cache 16 | /.eslintcache 17 | /connect.lock 18 | /coverage/ 19 | /libpeerconnection.log 20 | /npm-debug.log* 21 | /testem.log 22 | /yarn-error.log 23 | 24 | # ember-try 25 | /.node_modules.ember-try/ 26 | /bower.json.ember-try 27 | /npm-shrinkwrap.json.ember-try 28 | /package.json.ember-try 29 | /package-lock.json.ember-try 30 | /yarn.lock.ember-try 31 | 32 | # broccoli-debug 33 | /DEBUG/ 34 | 35 | #vitepress 36 | /docs/.vitepress/dist/ 37 | /docs/.vitepress/cache/ 38 | /docs/api 39 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | public-hoist-pattern[]=*prettier* 2 | public-hoist-pattern[]=*eslint* 3 | public-hoist-pattern[]=!@typescript-eslint/* 4 | public-hoist-pattern[]=*ember-template-lint* 5 | public-hoist-pattern[]=*stylelint* 6 | public-hoist-pattern[]=*@glint* 7 | strict-peer-dependencies=false 8 | -------------------------------------------------------------------------------- /.release-plan.json: -------------------------------------------------------------------------------- 1 | { 2 | "solution": { 3 | "ember-command": { 4 | "impact": "patch", 5 | "oldVersion": "2.0.6", 6 | "newVersion": "2.0.7", 7 | "constraints": [ 8 | { 9 | "impact": "patch", 10 | "reason": "Appears in changelog section :bug: Bug Fix" 11 | } 12 | ], 13 | "pkgJSONPath": "./ember-command/package.json" 14 | } 15 | }, 16 | "description": "## Release (2024-11-08)\n\nember-command 2.0.7 (patch)\n\n#### :bug: Bug Fix\n* `ember-command`, `test-app`\n * [#97](https://github.com/gossi/ember-command/pull/97) Fix another promise bug ([@gossi](https://github.com/gossi))\n\n#### Committers: 1\n- Thomas Gossmann ([@gossi](https://github.com/gossi))\n" 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[glimmer-ts]": { 3 | "editor.formatOnSave": true, 4 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 5 | }, 6 | "eslint.workingDirectories": [ 7 | { 8 | "mode": "auto" 9 | } 10 | ] 11 | // [ 12 | // "ember-command", 13 | // "test-app" 14 | // ] 15 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ember-command/CHANGELOG.md -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How To Contribute 2 | 3 | ## Installation 4 | 5 | * `git clone ` 6 | * `cd ember-command` 7 | * `yarn install` 8 | 9 | ## Linting 10 | 11 | * `yarn lint:hbs` 12 | * `yarn lint:js` 13 | * `yarn lint:js --fix` 14 | 15 | ## Running tests 16 | 17 | * `ember test` – Runs the test suite on the current Ember version 18 | * `ember test --server` – Runs the test suite in "watch mode" 19 | * `ember try:each` – Runs the test suite against multiple Ember versions 20 | 21 | ## Running the dummy application 22 | 23 | * `ember serve` 24 | * Visit the dummy application at [http://localhost:4200](http://localhost:4200). 25 | 26 | For more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/). 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ember-command/LICENSE.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ember-command/README.md -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release Process 2 | 3 | Releases in this repo are mostly automated using [release-plan](https://github.com/embroider-build/release-plan/). Once you label all your PRs correctly (see below) you will have an automatically generated PR that updates your CHANGELOG.md file and a `.release-plan.json` that is used to prepare the release once the PR is merged. 4 | 5 | ## Preparation 6 | 7 | Since the majority of the actual release process is automated, the remaining tasks before releasing are: 8 | 9 | - correctly labeling **all** pull requests that have been merged since the last release 10 | - updating pull request titles so they make sense to our users 11 | 12 | Some great information on why this is important can be found at [keepachangelog.com](https://keepachangelog.com/en/1.1.0/), but the overall 13 | guiding principle here is that changelogs are for humans, not machines. 14 | 15 | When reviewing merged PR's the labels to be used are: 16 | 17 | * breaking - Used when the PR is considered a breaking change. 18 | * enhancement - Used when the PR adds a new feature or enhancement. 19 | * bug - Used when the PR fixes a bug included in a previous release. 20 | * documentation - Used when the PR adds or updates documentation. 21 | * internal - Internal changes or things that don't fit in any other category. 22 | 23 | **Note:** `release-plan` requires that **all** PRs are labeled. If a PR doesn't fit in a category it's fine to label it as `internal` 24 | 25 | ## Release 26 | 27 | Once the prep work is completed, the actual release is straight forward: you just need to merge the open [Plan Release](https://github.com/gossi/ember-command/pulls?q=is%3Apr+is%3Aopen+%22Prepare+Release%22+in%3Atitle) PR 28 | -------------------------------------------------------------------------------- /docs/.vitepress/config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitepress"; 2 | 3 | import typedocSidebar from "../api/typedoc-sidebar.json"; 4 | 5 | // https://vitepress.dev/reference/site-config 6 | export default defineConfig({ 7 | title: "ember-command", 8 | description: "Integrate your Business Logic with Ember", 9 | base: "/ember-command/", 10 | themeConfig: { 11 | // https://vitepress.dev/reference/default-theme-config 12 | nav: [ 13 | { text: "Home", link: "/" }, 14 | { text: "Guides", link: "/getting-started" }, 15 | { text: "API", link: "/api/modules", activeMatch: "/api" }, 16 | ], 17 | 18 | outline: [2, 3], 19 | 20 | sidebar: { 21 | "/": [ 22 | { 23 | text: "Guides", 24 | items: [ 25 | { text: "Why ember-command?", link: "/why" }, 26 | { text: "Getting Started", link: "/getting-started" }, 27 | { text: "Installation", link: "/installation" }, 28 | { text: "Intended Usage", link: "/usage" }, 29 | ], 30 | }, 31 | { 32 | text: "Tutorial", 33 | items: [ 34 | { 35 | text: "Super Rentals Recommendations", 36 | link: "/super-rentals-recommendations", 37 | }, 38 | ], 39 | }, 40 | { 41 | text: "Commands", 42 | items: [ 43 | { text: "Functions", link: "/functions" }, 44 | { text: "Actions", link: "/actions" }, 45 | { text: "Command", link: "/command" }, 46 | { text: "Links", link: "/links" }, 47 | ], 48 | }, 49 | { 50 | text: "Using Commands", 51 | items: [ 52 | { text: "Composing", link: "/composing" }, 53 | { text: "Attaching to UI", link: "/ui" }, 54 | { text: "Testing", link: "/testing" }, 55 | ], 56 | }, 57 | ], 58 | "/api/": [ 59 | { 60 | text: "API", 61 | items: typedocSidebar, 62 | }, 63 | ], 64 | }, 65 | 66 | socialLinks: [ 67 | { icon: "github", link: "https://github.com/gossi/ember-command" }, 68 | ], 69 | }, 70 | }); 71 | -------------------------------------------------------------------------------- /docs/actions.md: -------------------------------------------------------------------------------- 1 | # Actions 2 | 3 | Actions are perfect to connect existing functions with the Ember's DI container 4 | or to wrap your service calls in functions. 5 | 6 | ## Wrapping Service Calls 7 | 8 | Here is a `Counter` service, which can increment and decrement. Let's connect it 9 | with a single-file-component. 10 | 11 | ::: info 12 | The best part: `action()` and 13 | [`ability()`](https://github.com/gossi/ember-ability) share the same API, so you 14 | only need to learn one. 15 | ::: 16 | 17 | ::: code-group 18 | 19 | ```gts [components/counter.gts] 20 | import { action } from 'ember-command'; 21 | import { ability } from 'ember-ability'; 22 | import { on } from '@ember/modifier'; 23 | 24 | const inc = action(({ services }) => () => { 25 | services.counter.inc(); 26 | }); 27 | 28 | const count = ability(({ services }) => () => { 29 | return services.counter.count; 30 | }); 31 | 32 | const Counter = 35 | 36 | export default Counter; 37 | ``` 38 | 39 | ```ts [services/counter.ts] 40 | import { tracked } from '@glimmer/tracking'; 41 | import Service from '@ember/service'; 42 | 43 | export default class CounterService extends Service { 44 | @tracked count = 0; 45 | 46 | inc = () => { 47 | this.count++; 48 | } 49 | 50 | dec = () => { 51 | this.count--; 52 | } 53 | } 54 | 55 | declare module '@ember/service' { 56 | export interface Registry { 57 | counter: CounterService; 58 | } 59 | } 60 | ``` 61 | 62 | ::: 63 | 64 | Due to Ember's helper infrastructure, an `action()` returns a factory, which in 65 | the template must be invoked, so Ember can associate the action with the helper 66 | manager that can find the owner. Thus, the *following code will break* as Ember 67 | is not able to make that association. 68 | 69 | ```hbs 70 | {{on "click" inc}} 71 | ``` 72 | 73 | ## Connect Business Logic 74 | 75 | Here is the request offer function for an addition to super-rentals example. The 76 | `requestOffer()` function contains the chunk of business logic (suprisingly these 77 | are most often only a few lines of code). The `requestOffer()` function can be 78 | properly unit tested to ensure it will find the right way into the backend with 79 | the expected payload. 80 | 81 | ```ts 82 | type Expose = object; // typed somewhere else 83 | 84 | export interface DataClient { 85 | sendCommand(name: string, payload: object): void; 86 | } 87 | 88 | export function requestOffer(recommendation: Expose, api: DataClient) { 89 | api.sendCommand('recommendations.request-offer', { recommendation }); 90 | } 91 | ``` 92 | 93 | `requestOffer()` function expects two parameters. The `api` comes as 94 | an Ember service and `recommendation` is an argument to the component in which 95 | the function is used. We are building a `requestOffer()` action as partial 96 | application and curry in the final parameter with `(fn)` during invocation. 97 | 98 | ::: warning 99 | 100 | Also remember to invoke the action in the template, so Ember can associate it 101 | with the backing helper manager. 102 | 103 | ::: 104 | 105 | ::: code-group 106 | 107 | ```gts [components/recommendation.gts] 108 | import { fn } from '@ember/helper'; 109 | import { action } from 'ember-command'; 110 | import { Button } from 'your-ui-package'; 111 | import { requestOffer as upstreamRequestOffer, type Expose } from 'your-businees-logic-package'; 112 | import type { TOC } from '@ember/component/template-only'; 113 | 114 | interface RecommendationSignature { 115 | Args: { 116 | recommendation: Expose; 117 | } 118 | } 119 | 120 | const requestOffer = action(({ services }) => (recommendation: Expose) => { 121 | upstreamRequestOffer(recommendation, services.data); 122 | }); 123 | 124 | const Recomendation: TOC = 127 | 128 | export default Recommendation; 129 | ``` 130 | 131 | ```ts [services/data.ts] 132 | import Service from '@ember/service'; 133 | import type { DataClient } from 'your-businees-logic-package'; 134 | 135 | export default class DataService extends Service implements DataClient { 136 | 137 | sendCommand(string: name, payload: object) { 138 | // ... implementation goes here 139 | } 140 | } 141 | 142 | declare module '@ember/service' { 143 | export interface Registry { 144 | data: DataService; 145 | } 146 | } 147 | ``` 148 | 149 | ::: 150 | 151 | ## Composing 152 | 153 | Actions can be composed with other commandables, such as links or functions. 154 | Here is the `inc()` with additional tracking: 155 | 156 | ```gts 157 | import { command, action } from 'ember-command'; 158 | import { track } from 'your-tracking-package'; 159 | import { on } from '@ember/modifier'; 160 | 161 | const inc = action(({ services }) => () => { 162 | services.counter.inc(); 163 | }); 164 | 165 | const Counter = 168 | 169 | export default Counter; 170 | ``` 171 | 172 | ::: info 173 | In contrast to Ember, the `(command)` helper is able to recognize an `action()` 174 | and thus doesn't need to be invoked. 175 | ::: 176 | -------------------------------------------------------------------------------- /docs/command.md: -------------------------------------------------------------------------------- 1 | # Command 2 | 3 | Commands as classes can use ember's dependency injection system as much as ember 4 | developers are used to. That's what the `Command` base class is 5 | for. Here is how we write our command from above with access to the data layer 6 | (an ember service) to fire off a command to the backend: 7 | 8 | ```ts 9 | import { inject as service } from '@ember/service'; 10 | import { Command } from 'ember-command'; 11 | import DataService from 'super-rentals/services/data'; 12 | 13 | export default class RequestOfferCommand extends Command { 14 | @service declare data: DataService; 15 | 16 | execute(): void { 17 | this.data.sendCommand('super-rentals.recommendations.request-offer', {...}); 18 | } 19 | } 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/composing.md: -------------------------------------------------------------------------------- 1 | # Composing 2 | 3 | In order to write clean and maintainable code, it's a good advice to keep the 4 | complexity low. In order to do so, functions should do one thing but do it well. 5 | 6 | Adding more features to an existing function is considered bad practice, as a 7 | function would exceed its responsibilities and is in danger of causing unwanted 8 | side-effects. To manage this and keep upon code quality promises, one such 9 | practice is to separate by the _what_ and the _how_ (by uncle bob). 10 | 11 | Here is a sample in plain JS. In order to prepare the checkout (the _what_), this 12 | requires to aquire customer details (1st _how_), payment method (2nd _how_) and 13 | shipping information (3rd _how_): 14 | 15 | ```js 16 | function prepareCheckout() { // the what 17 | aquireCustomerDetails(); // how #1 18 | aquirePaymentMethod(); // how #2 19 | aquireShipping(); // how #3 20 | } 21 | ``` 22 | 23 | Composing commands is providing this capability. Each commandable is turning 24 | into one _how_ whereas the composition of all them is the _what_. 25 | 26 | A command can be composed from [functions](./functions.md), 27 | [actions](./actions.md), [commands](./command.md), one [link](./links.md) or 28 | other composed commands (they are flattened in). 29 | 30 | ## Benefits 31 | 32 | Composability is really beneficial when working towards changing requirements. 33 | Once a command is written and attached to the UI, changing the functionality is 34 | keeping the code as is, but compose in another command. 35 | 36 | ## Invocation Styles 37 | 38 | Declarative: 39 | 40 | ```gts 41 | import { command, CommandElement } from 'ember-command'; 42 | import { track } from 'your-tracking-package'; 43 | import { link } from 'ember-link'; 44 | 45 | function sayHello() { 46 | console.log('Hi); 47 | } 48 | 49 | 52 | ``` 53 | 54 | Imperative: 55 | 56 | ```ts 57 | import { command, commandFor, LinkCommand } from 'ember-command'; 58 | import { TrackCommand } from 'your-tracking-package'; 59 | 60 | function sayHello() { 61 | console.log('Hi); 62 | } 63 | 64 | export default class MyComponent extends Component { 65 | @command doSth = commandFor([ 66 | sayHello, 67 | new LinkCommand({ route: 'application' }), 68 | new TrackCommand('said-hello') 69 | ]) 70 | } 71 | ``` 72 | -------------------------------------------------------------------------------- /docs/functions.md: -------------------------------------------------------------------------------- 1 | # Functions 2 | 3 | Functions are the most pure form to perform tasks. Dependency injection through 4 | parameters, a function has all the things needed to operate. `ember-command` can 5 | be composed of functions: 6 | 7 | ```gts 8 | import { on } from '@ember/modifier'; 9 | import { command } from 'ember-command'; 10 | 11 | function sayHello() { 12 | console.log('Hello); 13 | } 14 | 15 | 18 | ``` 19 | 20 | And using `(fn)` helper to pass it parameters: 21 | 22 | ```gts 23 | import { on } from '@ember/modifier'; 24 | import { fn } from '@ember/helper'; 25 | import { command } from 'ember-command'; 26 | 27 | function sayHello(name: string) { 28 | console.log('Hello', name); 29 | } 30 | 31 | 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Installation 4 | 5 | Install `ember-command` with: 6 | 7 | ```sh 8 | ember install ember-command 9 | ``` 10 | 11 | ## Usage 12 | 13 | The idea for `ember-command` is clearly to [separate your business logic from 14 | your UI](./why.md) by offering a couple of mechanics to do that. 15 | 16 | ### Actions 17 | 18 | Write an action that invokes a service within a [single file 19 | component](https://rfcs.emberjs.com/id/0779-first-class-component-templates). 20 | 21 | ```gts 22 | import { action } from 'ember-command'; 23 | import { on } from '@ember/modifier'; 24 | 25 | const inc = action(({ services }) => () => { 26 | services.counter.inc(); 27 | }); 28 | 29 | const Counter = 32 | 33 | export default Counter; 34 | ``` 35 | 36 | ### Composing 37 | 38 | Compose various commands together to form a primitive that can be passed around. 39 | This works well in combination with 40 | [`ember-link`](https://github.com/buschtoens/ember-link). 41 | 42 | Let's make a link and add tracking to it: 43 | 44 | ```gts 45 | import { command, action, CommandElement } from 'ember-command'; 46 | import { link } from 'ember-link'; 47 | 48 | const track = action(({ services }) => (event: string) => { 49 | services.tracking.track(event); 50 | }); 51 | 52 | const HomeLink = 58 | 59 | export default HomeLink; 60 | ``` 61 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: ember-command 7 | tagline: Integrate your Business Logic with Ember 8 | actions: 9 | - theme: brand 10 | text: Getting Started 11 | link: /getting-started 12 | - theme: alt 13 | text: Intended Usage 14 | link: /usage 15 | - theme: alt 16 | text: Separation of Business Logic and UI 17 | link: /why 18 | - theme: alt 19 | text: Super Rentals Tutorial 20 | link: /super-rentals-recommendations 21 | 22 | features: 23 | - title: Commands 24 | details: Implementation of the Command Design Pattern 25 | link: /command 26 | - title: Actions 27 | details: Lightweight commands, can interact with services 28 | link: /actions 29 | - title: Links 30 | details: Works with ember-link out of the box 31 | link: /links 32 | - title: Composition 33 | details: Compose multiple commands together 34 | link: /composing 35 | - title: Attaching to UI 36 | details: Create accessible UIs for your commands 37 | link: /ui 38 | --- 39 | 40 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | Install `ember-command` with: 4 | 5 | ```sh 6 | ember install ember-command 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/links.md: -------------------------------------------------------------------------------- 1 | # Links 2 | 3 | Commands can also be links, which the `` will render as `` 4 | element. The best solution for creating links is the 5 | [`ember-link`](https://github.com/buschtoens/ember-link) addon. 6 | Programmatically creating links with ember-link is a bit of a 7 | mouthful, like so: 8 | 9 | ```ts 10 | class RecommendationComponent extends Component { 11 | @service declare linkManager: LinkManagerService; 12 | 13 | get learnMore() { 14 | return this.linkManager.createLink({ route: 'recommendation.details' }); 15 | } 16 | } 17 | ``` 18 | 19 | Passing `learnMore` to your UI element would work straight ahead. 20 | `ember-command` comes with a more friendly syntax to create links 21 | programmatically for commands, which is the `LinkCommand` and be used as: 22 | 23 | ```ts 24 | import { command, LinkCommand } from 'ember-command'; 25 | 26 | class RecommendationComponent extends Component { 27 | @command leanMore = new LinkCommand({ route: 'recommendation.details' }); 28 | } 29 | ``` 30 | 31 | Compound commands work with links, too. Constructed as an array, as already used 32 | above with multiple commands: 33 | 34 | ```ts 35 | class RecommendationComponent extends Component { 36 | @command leanMoreLink = [ 37 | new LinkCommand({ route: 'recommendation.details' }), 38 | new TrackLearnMoreCommand(this.args.recommendation), 39 | ]; 40 | } 41 | ``` 42 | 43 | Whenever there is a link command present, the `` will render as 44 | ``. When there are multiple links present, the first one will be rendered, 45 | all successive ones will be dropped. 46 | -------------------------------------------------------------------------------- /docs/super-rentals-recommendations.md: -------------------------------------------------------------------------------- 1 | # Super Rentals Tutorial 2 | 3 | This tutorial in extending Super Rentals will teach on **preparing** your UI 4 | components, **writing commands**, **attaching** them your UI and **testing** 5 | them. 6 | 7 | This documentation is guided by product development and engineering for _Super 8 | Rentals Inc_ with CPO _Tomster_ and Frontend Engineer _Zoey_. 9 | 10 | > _Tomster_ realized a cohort of customers that are interested in seeing 11 | > personalized recommendations for rentals and put together a specification for 12 | > the feature. 13 | > Super Rentals shall be extended with a _recommendation_ section offering 14 | > personalized exposé to customers. Customers can _request offers_ and _learn 15 | > more_ about the object. 16 | >

17 | > _Tomster_ and _Zoey_ underlined the relevant nouns and verbs in the feature 18 | > specification to draw the domain terminology from it. The _recommendation_ is 19 | > the new domain object and _request offer_ and _learn more_ are the actions upon 20 | > that. 21 | >

22 | > Meanwhile the backend developers were busy delievering an endpoint that 23 | > implements the business logic for these actions. Now _Zoey's_ job is to 24 | > connect the UI to these endpoints. To dispatch the request, the `data` 25 | > service is used. 26 | 27 | ## Preparing UI Components 28 | 29 | To be conforming to accessibility standards, commands shall be rendered in their 30 | appropriate HTML element. `ember-command` provides the 31 | `` to do that. Your job is to integrate this component into your 32 | existing set of components. [WAI-ARIA 1.1 for Accessible Rich Internet 33 | Applications](https://www.w3.org/TR/wai-aria-1.1) explicitely mentions `button`, 34 | `menuitem` and `link` as the implementationable roles for the abstract super role 35 | [`command`](https://www.w3.org/TR/wai-aria-1.1/#command) (but there also may be 36 | more UI elements, that are receivers of commands). 37 | 38 | Let's make an example button component with the help of ``: 39 | 40 | ```gts 41 | import Component from '@glimmer/component'; 42 | import { CommandElement } from 'ember-command'; 43 | import type { TOC } from '@ember/component/template-only'; 44 | 45 | interface ButtonSignature { 46 | Element: HTMLButtonElement | HTMLAnchorElement | HTMLSpanElement; 47 | Args: { 48 | /** A command which will be invoked when the button is pushed */ 49 | push: Command; 50 | } 51 | } 52 | 53 | const Button: TOC = 58 | 59 | export default Button; 60 | ``` 61 | 62 | which we can use as: 63 | 64 | ```hbs 65 | 66 | ``` 67 | 68 | Yes a button is _pushed_ not _onClicked_ (Think about it: Do you _push_ or 69 | _click_ a button/switch to turn on the lights in your room?). 70 | 71 | > As _Zoey_ is caring about accessibility, she wants commands to be 72 | > represented as its appropriate element. 73 | 74 | Thanks to the `` the rendered element will adjust to either 75 | `
` or ` 379 | 380 | .. and somewhere else .. 381 | 382 | 383 | ``` 384 | 385 | Of course, `requestOffer` can be any format mentioned under [writing 386 | commands](#writing-commands) section. Also for links, you have a chance to do 387 | this in a template-only style: 388 | 389 | ```hbs 390 | 391 | ``` 392 | 393 | Just use the flavor you like the most. 394 | 395 | ## Testing Commands 396 | 397 | As commands are isolated and self-containing a business logic, we can write 398 | tests to specifically test for this. Let's test the tracking command using 399 | [`ember-sinon-qunit`](https://github.com/elwayman02/ember-sinon-qunit) to stub 400 | the `tracking` service: 401 | 402 | ```ts 403 | import { setupTest } from 'ember-qunit'; 404 | import { module, test } from 'qunit'; 405 | 406 | import { arrangeCommand } from 'ember-command/test-support'; 407 | import { TestContext } from 'ember-test-helpers'; 408 | 409 | import sinon from 'sinon'; 410 | 411 | import TrackingCommand from ''; 412 | 413 | module('Integration | Command | TrackingCommand', function (hooks) { 414 | setupTest(hooks); 415 | 416 | test('it tracks', async function (this: TestContext, assert) { 417 | this.owner.register('service:tracking', TrackingService); 418 | const trackingService = this.owner.lookup('service:tracking'); 419 | 420 | const stub = sinon.stub(trackingService, 'track'); 421 | const cmd = arrangeCommand(new TrackingCommand()); 422 | 423 | cmd.execute('hello'); 424 | 425 | assert.ok(stub.calledOnceWith('hello')); 426 | }); 427 | }); 428 | ``` 429 | 430 | The `arrangeCommand` is the testing equivalent to the `@command` decorator to 431 | attach the owner and wires up dependency injection. 432 | -------------------------------------------------------------------------------- /docs/testing.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | As commands are isolated and self-containing a business logic, we can write for 4 | this. There are integration tests, to test the commands itself and you can use 5 | instances of commands to test your UIs 6 | 7 | ## Integration Tests 8 | 9 | At first the `TrackingCommand` as a subject we want to test: 10 | 11 | ```ts 12 | import { service } from '@ember/service'; 13 | import { Command } from 'ember-command'; 14 | import type TrackingService from '/services/tracking'; 15 | 16 | export default class TrackCommand extends Command { 17 | @service declare tracking: TrackingService; 18 | 19 | execute(event: string, data?: unknown): void { 20 | this.tracking.track(event, data); 21 | } 22 | } 23 | ``` 24 | 25 | Let's test the tracking command using 26 | [`ember-sinon-qunit`](https://github.com/elwayman02/ember-sinon-qunit) to stub 27 | the `tracking` service: 28 | 29 | ```ts 30 | import { setupTest } from 'ember-qunit'; 31 | import { module, test } from 'qunit'; 32 | 33 | import { arrangeCommand } from 'ember-command/test-support'; 34 | import { TestContext } from 'ember-test-helpers'; 35 | 36 | import sinon from 'sinon'; 37 | 38 | import TrackingCommand from ''; 39 | 40 | module('Integration | Command | TrackingCommand', function (hooks) { 41 | setupTest(hooks); 42 | 43 | test('it tracks', async function (this: TestContext, assert) { 44 | this.owner.register('service:tracking', TrackingService); 45 | const trackingService = this.owner.lookup('service:tracking'); 46 | 47 | const stub = sinon.stub(trackingService, 'track'); 48 | const cmd = arrangeCommand(new TrackingCommand()); 49 | 50 | cmd.execute('hello'); 51 | 52 | assert.ok(stub.calledOnceWith('hello')); 53 | }); 54 | }); 55 | ``` 56 | 57 | The `arrangeCommand` is the testing equivalent to the `@command` decorator to 58 | attach the owner and wires up dependency injection. 59 | 60 | ## Rendering Tests 61 | 62 | When your components accept commands as arguments, here is how to test them: 63 | 64 | ```gts 65 | import { setupTest } from 'ember-qunit'; 66 | import { module, test } from 'qunit'; 67 | 68 | import { arrangeCommandInstance } from 'ember-command/test-support'; 69 | import { TestContext } from 'ember-test-helpers'; 70 | 71 | import sinon from 'sinon'; 72 | 73 | import TrackingCommand from ''; 74 | import YourComponnent from ''; 75 | 76 | module('Rendering | YourComponent', function (hooks) { 77 | setupTest(hooks); 78 | 79 | test('it triggers a @cmd', async function (this: TestContext, assert) { 80 | const cmd = arrangeCommandInstance(new TrackingCommand()); 81 | const stub = sinon.stub(cmd, 'execute'); 82 | 83 | await render( 84 | 87 | ); 88 | 89 | await click('button'); 90 | 91 | assert.ok(stub.calledOnce); 92 | }); 93 | }); 94 | ``` 95 | -------------------------------------------------------------------------------- /docs/ui.md: -------------------------------------------------------------------------------- 1 | # Attaching to UI 2 | 3 | `ember-command` provides you with a `` component to render 4 | commands in their appropriate HTML element to make it accessibility compliant. 5 | [WAI-ARIA 1.1 for Accessible Rich Internet 6 | Applications](https://www.w3.org/TR/wai-aria-1.1) explicitely mentions 7 | `button`, `menuitem` and `link` as the implementationable roles for the abstract 8 | super role [`command`](https://www.w3.org/TR/wai-aria-1.1/#command) (but there 9 | also may be more UI elements, that are receivers of commands). 10 | 11 | As such `` acts as building block to your components. Let's make 12 | an example button component: 13 | 14 | ```gts 15 | import Component from '@glimmer/component'; 16 | import { CommandElement } from 'ember-command'; 17 | import type { TOC } from '@ember/component/template-only'; 18 | 19 | interface ButtonSignature { 20 | Element: HTMLButtonElement | HTMLAnchorElement | HTMLSpanElement; 21 | Args: { 22 | /** A command which will be invoked when the button is pushed */ 23 | push: Command; 24 | } 25 | } 26 | 27 | const Button: TOC = 32 | 33 | export default Button; 34 | ``` 35 | 36 | which we can use as: 37 | 38 | ```hbs 39 | 40 | ``` 41 | 42 | Thanks to the `` the rendered element will adjust to either 43 | `` or ` 4 | 5 | 8 | 9 | 12 | 13 | 16 | 17 |
18 | 19 | Counter: 20 | {{this.counterValue}} 21 | 22 | 25 | 28 | 29 |
30 | 31 | Cook a curry: 32 | 33 | 36 | 39 | 42 | 43 |
44 | 45 | Links to: 46 | 47 | 50 | - 51 | 54 | - 55 | 58 | - 59 | -------------------------------------------------------------------------------- /test-app/app/components/outer-demo/component.ts: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | 3 | import { command, commandFor } from 'ember-command'; 4 | 5 | import CookCurryCommand from './cook-curry-command'; 6 | 7 | export default class OuterDemo extends Component { 8 | @command cookTheCurry = commandFor(new CookCurryCommand()); 9 | } 10 | 11 | declare module '@glint/environment-ember-loose/registry' { 12 | export default interface Registry { 13 | // eslint-disable-next-line @typescript-eslint/naming-convention 14 | OuterDemo: typeof OuterDemo; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test-app/app/components/outer-demo/cook-curry-command.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'ember-command'; 2 | 3 | export default class CookCurryCommand extends Command { 4 | execute(curry: string): void { 5 | // eslint-disable-next-line no-console 6 | console.log(`cooking ${curry} curry`); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test-app/app/components/outer-demo/template.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test-app/app/config/environment.d.ts: -------------------------------------------------------------------------------- 1 | export default config; 2 | 3 | /** 4 | * Type declarations for 5 | * import config from 'my-app/config/environment' 6 | */ 7 | declare const config: { 8 | environment: string; 9 | modulePrefix: string; 10 | podModulePrefix: string; 11 | locationType: 'history' | 'hash' | 'none' | 'auto'; 12 | rootURL: string; 13 | APP: Record; 14 | }; 15 | -------------------------------------------------------------------------------- /test-app/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | {{content-for "head-footer"}} 16 | 17 | 18 | {{content-for "body"}} 19 | 20 | 21 | 22 | 23 | {{content-for "body-footer"}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /test-app/app/router.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-invalid-this */ 2 | import EmberRouter from '@ember/routing/router'; 3 | 4 | import config from './config/environment'; 5 | 6 | export default class Router extends EmberRouter { 7 | location = config.locationType; 8 | rootURL = config.rootURL; 9 | } 10 | 11 | Router.map(function () { 12 | this.route('route-a'); 13 | this.route('route-b'); 14 | this.route('route-c'); 15 | }); 16 | -------------------------------------------------------------------------------- /test-app/app/routes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gossi/ember-command/b169fab3289e459017972cbd551ac617bd2d6f8c/test-app/app/routes/.gitkeep -------------------------------------------------------------------------------- /test-app/app/services/counter.ts: -------------------------------------------------------------------------------- 1 | import { tracked } from '@glimmer/tracking'; 2 | import Service from '@ember/service'; 3 | 4 | export default class CounterService extends Service { 5 | @tracked counter = 0; 6 | } 7 | 8 | declare module '@ember/service' { 9 | export interface Registry { 10 | counter: CounterService; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test-app/app/styles/app.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gossi/ember-command/b169fab3289e459017972cbd551ac617bd2d6f8c/test-app/app/styles/app.css -------------------------------------------------------------------------------- /test-app/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 |

Welcome to Actionables

2 | 3 | 4 | 5 | {{outlet}} -------------------------------------------------------------------------------- /test-app/app/templates/route-a.hbs: -------------------------------------------------------------------------------- 1 |

Route A

-------------------------------------------------------------------------------- /test-app/app/templates/route-b.hbs: -------------------------------------------------------------------------------- 1 |

Route B

-------------------------------------------------------------------------------- /test-app/app/templates/route-c.hbs: -------------------------------------------------------------------------------- 1 |

Route C

-------------------------------------------------------------------------------- /test-app/config/ember-cli-update.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.0.0", 3 | "packages": [ 4 | { 5 | "name": "ember-cli", 6 | "version": "3.25.2", 7 | "blueprints": [ 8 | { 9 | "name": "addon", 10 | "outputRepo": "https://github.com/ember-cli/ember-addon-output", 11 | "codemodsSource": "ember-addon-codemods-manifest@1", 12 | "isBaseBlueprint": true, 13 | "options": [ 14 | "--yarn" 15 | ] 16 | } 17 | ] 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /test-app/config/ember-try.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getChannelURL = require('ember-source-channel-url'); 4 | const { embroiderSafe, embroiderOptimized } = require('@embroider/test-setup'); 5 | 6 | module.exports = async function () { 7 | return { 8 | usePnpm: true, 9 | scenarios: [ 10 | { 11 | name: 'ember-3.28', 12 | npm: { 13 | devDependencies: { 14 | '@ember/test-helpers': '^2.9.3', 15 | 'ember-cli': '~3.28.6', 16 | 'ember-source': '~3.28.12', 17 | 'ember-qunit': '^6.2.0' 18 | } 19 | } 20 | }, 21 | { 22 | name: 'ember-4.4', 23 | npm: { 24 | devDependencies: { 25 | 'ember-source': '~4.4.0' 26 | } 27 | } 28 | }, 29 | { 30 | name: 'ember-4.8', 31 | npm: { 32 | devDependencies: { 33 | 'ember-source': '~4.8.0' 34 | } 35 | } 36 | }, 37 | { 38 | name: 'ember-4.12', 39 | npm: { 40 | devDependencies: { 41 | 'ember-source': '~4.12.0' 42 | } 43 | } 44 | }, 45 | { 46 | name: 'ember-release', 47 | npm: { 48 | devDependencies: { 49 | 'ember-source': await getChannelURL('release') 50 | } 51 | } 52 | }, 53 | { 54 | name: 'ember-beta', 55 | npm: { 56 | devDependencies: { 57 | 'ember-source': await getChannelURL('beta') 58 | } 59 | } 60 | }, 61 | { 62 | name: 'ember-canary', 63 | npm: { 64 | allowedToFail: true, 65 | devDependencies: { 66 | 'ember-source': await getChannelURL('canary') 67 | } 68 | } 69 | }, 70 | embroiderSafe(), 71 | embroiderOptimized(), 72 | { 73 | name: 'ember-link-v1', 74 | npm: { 75 | devDependencies: { 76 | 'ember-link': '^1.3.1' 77 | } 78 | } 79 | }, 80 | { 81 | name: 'ember-link-v2', 82 | npm: { 83 | devDependencies: { 84 | 'ember-link': '^2.1.0' 85 | } 86 | } 87 | } 88 | ] 89 | }; 90 | }; 91 | -------------------------------------------------------------------------------- /test-app/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (environment) { 4 | const ENV = { 5 | modulePrefix: 'test-app', 6 | environment, 7 | rootURL: '/', 8 | locationType: 'history', 9 | EmberENV: { 10 | FEATURES: { 11 | // Here you can enable experimental features on an ember canary build 12 | // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true 13 | }, 14 | EXTEND_PROTOTYPES: { 15 | // Prevent Ember Data from overriding Date.parse. 16 | Date: false 17 | } 18 | }, 19 | 20 | APP: { 21 | // Here you can pass flags/options to your application instance 22 | // when it is created 23 | } 24 | }; 25 | 26 | if (environment === 'development') { 27 | // ENV.APP.LOG_RESOLVER = true; 28 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 29 | // ENV.APP.LOG_TRANSITIONS = true; 30 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 31 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 32 | } 33 | 34 | if (environment === 'test') { 35 | // Testem prefers this... 36 | ENV.locationType = 'none'; 37 | 38 | // keep test console output quieter 39 | ENV.APP.LOG_ACTIVE_GENERATION = false; 40 | ENV.APP.LOG_VIEW_LOOKUPS = false; 41 | 42 | ENV.APP.rootElement = '#ember-testing'; 43 | ENV.APP.autoboot = false; 44 | } 45 | 46 | if (environment === 'production') { 47 | // here you can enable a production-specific feature 48 | } 49 | 50 | return ENV; 51 | }; 52 | -------------------------------------------------------------------------------- /test-app/config/optional-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "application-template-wrapper": false, 3 | "default-async-observers": true, 4 | "jquery-integration": false, 5 | "template-only-glimmer-components": true 6 | } 7 | -------------------------------------------------------------------------------- /test-app/config/targets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const browsers = ['last 1 Chrome versions', 'last 1 Firefox versions', 'last 1 Safari versions']; 4 | 5 | module.exports = { 6 | browsers 7 | }; 8 | -------------------------------------------------------------------------------- /test-app/ember-cli-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EmberApp = require('ember-cli/lib/broccoli/ember-app'); 4 | const packageJson = require('./package'); 5 | 6 | module.exports = function (defaults) { 7 | const app = new EmberApp(defaults, { 8 | // Add options here 9 | autoImport: { 10 | watchDependencies: Object.keys(packageJson.dependencies) 11 | }, 12 | babel: { 13 | sourceMaps: 'inline', 14 | plugins: [ 15 | require.resolve('ember-concurrency/async-arrow-task-transform'), 16 | '@babel/plugin-transform-class-static-block' 17 | ] 18 | } 19 | }); 20 | 21 | const { maybeEmbroider } = require('@embroider/test-setup'); 22 | 23 | return maybeEmbroider(app); 24 | }; 25 | -------------------------------------------------------------------------------- /test-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-app", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Commands for Ember", 6 | "keywords": [ 7 | "command", 8 | "link", 9 | "action", 10 | "cqs" 11 | ], 12 | "license": "MIT", 13 | "author": "Thomas Gossmann", 14 | "scripts": { 15 | "dev": "concurrently 'npm:dev:*'", 16 | "dev:test-app": "ember serve -e test -p 4300", 17 | "dev:package": "pnpm run --filter='ember-command' start", 18 | "lint": "concurrently -g 'npm:lint:*(!fix)'", 19 | "lint:fix": "concurrently -g 'npm:lint:*:fix'", 20 | "lint:hbs": "ember-template-lint . --no-error-on-unmatched-pattern", 21 | "lint:hbs:fix": "ember-template-lint . --fix --no-error-on-unmatched-pattern", 22 | "lint:js": "eslint . --cache", 23 | "lint:js:fix": "eslint . --fix", 24 | "_lint:types": "tsc --noEmit", 25 | "glint": "glint", 26 | "test": "ember test -tp 0" 27 | }, 28 | "dependencies": { 29 | "ember-command": "workspace:*" 30 | }, 31 | "dependenciesMeta": { 32 | "ember-command": { 33 | "injected": true 34 | } 35 | }, 36 | "devDependencies": { 37 | "@babel/core": "^7.27.1", 38 | "@babel/plugin-transform-class-static-block": "^7.27.1", 39 | "@ember/optional-features": "^2.2.0", 40 | "@ember/string": "^3.1.1", 41 | "@ember/test-helpers": "^4.0.5", 42 | "@embroider/test-setup": "^4.0.0", 43 | "@embroider/webpack": "^4.0.9", 44 | "@embroider/compat": "^3.7.1", 45 | "@embroider/macros": "^1.16.10", 46 | "@glimmer/component": "^1.1.2", 47 | "@glimmer/tracking": "^1.1.2", 48 | "@glint/core": "^1.5.0", 49 | "@glint/template": "^1.5.0", 50 | "@glint/environment-ember-loose": "^1.5.0", 51 | "@glint/environment-ember-template-imports": "1.5.0", 52 | "@gossi/config-eslint": "^0.13.0", 53 | "@gossi/config-prettier": "^0.9.1", 54 | "@gossi/config-template-lint": "^0.8.1", 55 | "@tsconfig/ember": "^3.0.8", 56 | "@types/qunit": "^2.19.6", 57 | "@types/sinon": "^10.0.15", 58 | "@typescript-eslint/eslint-plugin": "^8.18.2", 59 | "@typescript-eslint/parser": "^8.18.2", 60 | "broccoli-asset-rev": "^3.0.0", 61 | "concurrently": "^8.2.2", 62 | "ember-auto-import": "^2.10.0", 63 | "ember-cli": "~6.1.0", 64 | "ember-cli-babel": "^7.26.11", 65 | "ember-cli-dependency-checker": "^3.3.3", 66 | "ember-cli-htmlbars": "^6.3.0", 67 | "ember-cli-inject-live-reload": "^2.1.0", 68 | "ember-cli-sri": "^2.1.1", 69 | "ember-cli-typescript": "^5.3.0", 70 | "ember-concurrency": "^4.0.2", 71 | "ember-disable-prototype-extensions": "^1.1.3", 72 | "ember-element-helper": "^0.8.6", 73 | "ember-link": "^3.3.0", 74 | "ember-load-initializers": "^2.1.2", 75 | "ember-qunit": "^7.0.0", 76 | "ember-resolver": "^10.1.1", 77 | "ember-sinon-qunit": "^7.5.0", 78 | "ember-source": "~5.12.0", 79 | "ember-source-channel-url": "^3.0.0", 80 | "ember-template-imports": "^3.4.2", 81 | "ember-template-lint": "^6.0.0", 82 | "ember-try": "^2.0.0", 83 | "eslint": "^8.57.1", 84 | "eslint-plugin-ember": "^12.3.3", 85 | "loader.js": "^4.7.0", 86 | "prettier": "^3.4.2", 87 | "qunit": "^2.22.0", 88 | "qunit-dom": "^2.0.0", 89 | "sinon": "^19.0.2", 90 | "typescript": "^5.7.2", 91 | "webpack": "^5.97.1" 92 | }, 93 | "engines": { 94 | "node": ">= 20.*" 95 | }, 96 | "ember": { 97 | "edition": "octane" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /test-app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /test-app/testem.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | test_page: 'tests/index.html?hidepassed', 5 | disable_watching: true, 6 | launch_in_ci: ['Chrome'], 7 | launch_in_dev: ['Chrome'], 8 | browser_args: { 9 | Chrome: { 10 | ci: [ 11 | // --no-sandbox is needed when running Chrome inside a container 12 | process.env.CI ? '--no-sandbox' : undefined, 13 | '--headless', 14 | '--disable-gpu', 15 | '--disable-dev-shm-usage', 16 | '--disable-software-rasterizer', 17 | '--mute-audio', 18 | '--remote-debugging-port=0', 19 | '--window-size=1440,900' 20 | ].filter(Boolean) 21 | } 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /test-app/tests/-owner.ts: -------------------------------------------------------------------------------- 1 | import { dependencySatisfies, importSync, macroCondition } from '@embroider/macros'; 2 | 3 | import type { getOwner as getOwnerType, setOwner as setOwnerType } from '@ember/owner'; 4 | 5 | interface OwnerModule { 6 | setOwner: typeof setOwnerType; 7 | getOwner: typeof getOwnerType; 8 | } 9 | 10 | const owner = ( 11 | macroCondition(dependencySatisfies('ember-source', '>=4.10')) 12 | ? importSync('@ember/owner') 13 | : importSync('@ember/application') 14 | ) as OwnerModule; 15 | 16 | const { getOwner, setOwner } = owner; 17 | 18 | export { getOwner, setOwner }; 19 | -------------------------------------------------------------------------------- /test-app/tests/commands/task-command.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'ember-command'; 2 | import { dropTask, timeout } from 'ember-concurrency'; 3 | 4 | interface Bag { 5 | carry: boolean; 6 | } 7 | 8 | export default class TaskCommand extends Command { 9 | private bag: Bag; 10 | 11 | constructor(bag: Bag) { 12 | super(); 13 | this.bag = bag; 14 | } 15 | 16 | async execute() { 17 | await this.changeBag.perform(); 18 | } 19 | 20 | changeBag = dropTask(async () => { 21 | await timeout(250); 22 | 23 | this.bag.carry = true; 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /test-app/tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 |
25 |
26 |
27 |
28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {{content-for "body-footer"}} 38 | {{content-for "test-body-footer"}} 39 | 40 | 41 | -------------------------------------------------------------------------------- /test-app/tests/integration/commands/counter-test.ts: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupTest } from 'ember-qunit'; 3 | 4 | import CounterDecrementCommand from 'test-app/components/command-demo/counter-decrement-command'; 5 | import CounterIncrementCommand from 'test-app/components/command-demo/counter-increment-command'; 6 | import CounterService from 'test-app/services/counter'; 7 | 8 | import { arrangeCommand } from 'ember-command/test-support'; 9 | 10 | import type { TestContext } from '@ember/test-helpers'; 11 | 12 | module('Integration | Command | Counter', function (hooks) { 13 | setupTest(hooks); 14 | 15 | test('it in- and decrements', function (this: TestContext, assert) { 16 | this.owner.register('service:counter', CounterService); 17 | 18 | const counterService = this.owner.lookup('service:counter'); 19 | 20 | assert.strictEqual(counterService.counter, 0); 21 | 22 | const inc = arrangeCommand(new CounterIncrementCommand()); 23 | const dec = arrangeCommand(new CounterDecrementCommand()); 24 | 25 | inc.execute(); 26 | assert.strictEqual(counterService.counter, 1); 27 | 28 | dec.execute(); 29 | assert.strictEqual(counterService.counter, 0); 30 | 31 | dec.execute(); 32 | assert.strictEqual(counterService.counter, -1); 33 | 34 | inc.execute(); 35 | assert.strictEqual(counterService.counter, 0); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test-app/tests/integration/commands/foobar-test.ts: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupTest } from 'ember-qunit'; 3 | 4 | import sinon from 'sinon'; 5 | import FooBarLogCommand from 'test-app/components/command-demo/foobar-log-command'; 6 | 7 | import { arrangeCommand } from 'ember-command/test-support'; 8 | 9 | import type { TestContext } from '@ember/test-helpers'; 10 | 11 | module('Integration | Command | FooBar', function (hooks) { 12 | setupTest(hooks); 13 | 14 | test('it logs "foobar"', function (this: TestContext, assert) { 15 | const stub = sinon.stub(console, 'log'); 16 | const cmd = arrangeCommand(new FooBarLogCommand()); 17 | 18 | cmd.execute(); 19 | 20 | assert.ok(stub.calledOnceWith('foobar')); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test-app/tests/integration/commands/task-test.ts: -------------------------------------------------------------------------------- 1 | import { type TestContext } from '@ember/test-helpers'; 2 | import { module, test } from 'qunit'; 3 | import { setupTest } from 'ember-qunit'; 4 | 5 | import { arrangeCommand } from 'ember-command/test-support'; 6 | 7 | import TaskCommand from '../../commands/task-command'; 8 | 9 | module('Integration | Command | Task', function (hooks) { 10 | setupTest(hooks); 11 | 12 | test('it waits for change', async function (this: TestContext, assert) { 13 | const bag = { 14 | carry: false 15 | }; 16 | 17 | assert.notOk(bag.carry); 18 | 19 | const cmd = arrangeCommand(new TaskCommand(bag)); 20 | 21 | await cmd.execute(); 22 | 23 | assert.ok(bag.carry); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test-app/tests/integration/helpers/command-test.ts: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import { click, render } from '@ember/test-helpers'; 3 | import { hbs } from 'ember-cli-htmlbars'; 4 | import { module, test } from 'qunit'; 5 | import { setupRenderingTest } from 'ember-qunit'; 6 | 7 | import { LinkCommand } from 'ember-command'; 8 | import sinon from 'sinon'; 9 | import FooBarLogCommand from 'test-app/components/command-demo/foobar-log-command'; 10 | import PushLogCommand from 'test-app/components/command-demo/push-log-command'; 11 | 12 | import { arrangeCommand } from 'ember-command/test-support'; 13 | import { linkFor, setupLink } from 'ember-link/test-support'; 14 | 15 | import type { TestContext as BaseTestContext } from '@ember/test-helpers'; 16 | import type { Commandable } from 'ember-command'; 17 | import type { TestLink } from 'ember-link/test-support'; 18 | import type { SinonSpy } from 'sinon'; 19 | 20 | interface TestContext extends BaseTestContext { 21 | command: Commandable; 22 | commandA: Commandable; 23 | commandB: Commandable; 24 | link: TestLink; 25 | } 26 | 27 | module('Integration | Helper | command', function (hooks) { 28 | setupRenderingTest(hooks); 29 | setupLink(hooks); 30 | 31 | test('it renders for a function', async function (this: TestContext, assert) { 32 | this.command = sinon.spy(); 33 | await render(hbs``); 34 | 35 | assert.dom('[data-test-commander]').hasTagName('button'); 36 | assert.dom('[data-test-commander]').hasAttribute('type'); 37 | assert.dom('[data-test-commander]').doesNotHaveAttribute('href'); 38 | }); 39 | 40 | test('it renders for a link', async function (this: TestContext, assert) { 41 | this.link = linkFor('some.route'); 42 | await render(hbs``); 43 | 44 | assert.dom('[data-test-commander]').hasTagName('a'); 45 | assert.dom('[data-test-commander]').hasAttribute('href'); 46 | 47 | await render(hbs``); 48 | assert.dom('[data-test-commander]').hasTagName('a'); 49 | assert.dom('[data-test-commander]').hasAttribute('href'); 50 | }); 51 | 52 | test('it renders for a command', async function (this: TestContext, assert) { 53 | this.command = arrangeCommand(new PushLogCommand()); 54 | await render(hbs``); 55 | 56 | assert.dom('[data-test-commander]').hasTagName('button'); 57 | assert.dom('[data-test-commander]').hasAttribute('type'); 58 | assert.dom('[data-test-commander]').doesNotHaveAttribute('href'); 59 | }); 60 | 61 | test('it renders for a link command', async function (this: TestContext, assert) { 62 | this.owner.register('route:test-route', class extends Route {}); 63 | this.command = arrangeCommand(new LinkCommand({ route: 'test-route' })); 64 | await render(hbs``); 65 | 66 | assert.dom('[data-test-commander]').hasTagName('a'); 67 | assert.dom('[data-test-commander]').hasAttribute('href'); 68 | }); 69 | 70 | // compounds 71 | 72 | test('it renders for compound command', async function (this: TestContext, assert) { 73 | this.commandA = arrangeCommand(new PushLogCommand()); 74 | this.commandB = arrangeCommand(new FooBarLogCommand()); 75 | await render( 76 | hbs`` 77 | ); 78 | 79 | assert.dom('[data-test-commander]').hasTagName('button'); 80 | assert.dom('[data-test-commander]').hasAttribute('type'); 81 | assert.dom('[data-test-commander]').doesNotHaveAttribute('href'); 82 | }); 83 | 84 | test('it renders for compound link', async function (this: TestContext, assert) { 85 | this.owner.register('route:test-route', class extends Route {}); 86 | this.owner.register('route:test-route2', class extends Route {}); 87 | this.commandA = arrangeCommand(new LinkCommand({ route: 'test-route' })); 88 | this.commandB = arrangeCommand(new LinkCommand({ route: 'test-route2' })); 89 | await render( 90 | hbs`` 91 | ); 92 | 93 | assert.dom('[data-test-commander]').hasTagName('a'); 94 | assert.dom('[data-test-commander]').hasAttribute('href'); 95 | }); 96 | 97 | test('it renders for compound command + link', async function (this: TestContext, assert) { 98 | this.owner.register('route:test-route', class extends Route {}); 99 | this.commandA = arrangeCommand(new LinkCommand({ route: 'test-route' })); 100 | this.commandB = arrangeCommand(new FooBarLogCommand()); 101 | await render( 102 | hbs`` 103 | ); 104 | 105 | assert.dom('[data-test-commander]').hasTagName('a'); 106 | assert.dom('[data-test-commander]').hasAttribute('href'); 107 | 108 | this.commandA = arrangeCommand(linkFor('test-route')); 109 | this.commandB = arrangeCommand(new FooBarLogCommand()); 110 | 111 | await render( 112 | hbs`` 113 | ); 114 | 115 | assert.dom('[data-test-commander]').hasTagName('a'); 116 | assert.dom('[data-test-commander]').hasAttribute('href'); 117 | }); 118 | 119 | // invoke 120 | test('invoke function', async function (this: TestContext, assert) { 121 | this.command = sinon.spy(); 122 | await render(hbs``); 123 | 124 | await click('[data-test-commander]'); 125 | assert.ok((this.command as SinonSpy).calledOnce); 126 | 127 | this.commandA = arrangeCommand(new PushLogCommand()); 128 | this.commandB = arrangeCommand(new FooBarLogCommand()); 129 | await render( 130 | hbs`` 131 | ); 132 | 133 | assert.dom('[data-test-commander]').hasTagName('button'); 134 | assert.dom('[data-test-commander]').hasAttribute('type'); 135 | assert.dom('[data-test-commander]').doesNotHaveAttribute('href'); 136 | }); 137 | 138 | test('invoke command', async function (this: TestContext, assert) { 139 | const foo = new FooBarLogCommand(); 140 | const stub = sinon.stub(foo, 'execute'); 141 | 142 | this.command = arrangeCommand(foo); 143 | await render(hbs``); 144 | 145 | await click('[data-test-commander]'); 146 | assert.ok(stub.calledOnce); 147 | }); 148 | 149 | test('invoke link', async function (this: TestContext, assert) { 150 | this.link = linkFor('some.route'); 151 | 152 | this.link.onTransitionTo = () => { 153 | assert.step('link clicked'); 154 | }; 155 | 156 | await render(hbs``); 157 | 158 | await click('[data-test-commander]'); 159 | assert.verifySteps(['link clicked']); 160 | }); 161 | }); 162 | -------------------------------------------------------------------------------- /test-app/tests/rendering/action-test.gts: -------------------------------------------------------------------------------- 1 | import { fn } from '@ember/helper'; 2 | import { on } from '@ember/modifier'; 3 | import { click, render } from '@ember/test-helpers'; 4 | import { module, test } from 'qunit'; 5 | import { setupRenderingTest } from 'ember-qunit'; 6 | 7 | import { action, command } from 'ember-command'; 8 | 9 | import type { TOC } from '@ember/component/template-only'; 10 | import type { TestContext } from '@ember/test-helpers'; 11 | import type CounterService from 'test-app/services/counter'; 12 | 13 | const inc = action(({ services }) => { 14 | return (amount: number) => { 15 | services.counter.counter += amount; 16 | }; 17 | }); 18 | 19 | const incOne = action(({ services }) => { 20 | return () => { 21 | services.counter.counter += 1; 22 | }; 23 | }); 24 | 25 | interface StepSignature { 26 | Element: HTMLButtonElement; 27 | Args: { push: (amount: number) => void; inc: number }; 28 | Blocks: { 29 | default: []; 30 | }; 31 | } 32 | 33 | // eslint-disable-next-line @typescript-eslint/naming-convention 34 | const Step: TOC = ; 37 | 38 | module('Rendering | action', function (hooks) { 39 | setupRenderingTest(hooks); 40 | 41 | module('(action)', function () { 42 | test('plain invocation', async function (this: TestContext, assert) { 43 | const counter = this.owner.lookup('service:counter') as CounterService; 44 | 45 | await render( 46 | 49 | ); 50 | 51 | assert.equal(counter.counter, 0, 'Counter is 0'); 52 | 53 | await click('button'); 54 | 55 | assert.equal(counter.counter, 1, 'Counter is 1'); 56 | }); 57 | 58 | test('with parameters', async function (this: TestContext, assert) { 59 | const counter = this.owner.lookup('service:counter') as CounterService; 60 | 61 | await render( 62 | 65 | ); 66 | 67 | assert.equal(counter.counter, 0, 'Counter is 0'); 68 | 69 | await click('button'); 70 | 71 | assert.equal(counter.counter, 10, 'Counter is 10'); 72 | }); 73 | }); 74 | 75 | module('(fn (action))', function () { 76 | test('plain invocation', async function (this: TestContext, assert) { 77 | const counter = this.owner.lookup('service:counter') as CounterService; 78 | 79 | await render( 80 | 83 | ); 84 | 85 | assert.equal(counter.counter, 0, 'Counter is 0'); 86 | 87 | await click('button'); 88 | 89 | assert.equal(counter.counter, 1, 'Counter is 1'); 90 | }); 91 | 92 | test('curried with (fn)', async function (this: TestContext, assert) { 93 | const counter = this.owner.lookup('service:counter') as CounterService; 94 | 95 | await render( 96 | 99 | ); 100 | 101 | assert.equal(counter.counter, 0, 'Counter is 0'); 102 | 103 | await click('button'); 104 | 105 | assert.equal(counter.counter, 10, 'Counter is 10'); 106 | }); 107 | 108 | test('with parameters', async function (this: TestContext, assert) { 109 | const counter = this.owner.lookup('service:counter') as CounterService; 110 | 111 | await render( 112 | 115 | ); 116 | 117 | assert.equal(counter.counter, 0, 'Counter is 0'); 118 | 119 | await click('button'); 120 | 121 | assert.equal(counter.counter, 10, 'Counter is 10'); 122 | }); 123 | }); 124 | 125 | module('(command action)', function () { 126 | test('plain invocation', async function (this: TestContext, assert) { 127 | const counter = this.owner.lookup('service:counter') as CounterService; 128 | 129 | await render( 130 | 133 | ); 134 | 135 | assert.equal(counter.counter, 0, 'Counter is 0'); 136 | 137 | await click('button'); 138 | 139 | assert.equal(counter.counter, 1, 'Counter is 1'); 140 | }); 141 | 142 | test('curried invocation', async function (this: TestContext, assert) { 143 | const counter = this.owner.lookup('service:counter') as CounterService; 144 | 145 | await render( 146 | 149 | ); 150 | 151 | assert.equal(counter.counter, 0, 'Counter is 0'); 152 | 153 | await click('button'); 154 | 155 | assert.equal(counter.counter, 10, 'Counter is 10'); 156 | }); 157 | 158 | test('with parameters', async function (this: TestContext, assert) { 159 | const counter = this.owner.lookup('service:counter') as CounterService; 160 | 161 | await render( 162 | 165 | ); 166 | 167 | assert.equal(counter.counter, 0, 'Counter is 0'); 168 | 169 | await click('button'); 170 | 171 | assert.equal(counter.counter, 10, 'Counter is 10'); 172 | }); 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /test-app/tests/rendering/command-element-test.ts: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import { click, render } from '@ember/test-helpers'; 3 | import { hbs } from 'ember-cli-htmlbars'; 4 | import { module, test } from 'qunit'; 5 | import { setupRenderingTest } from 'ember-qunit'; 6 | 7 | import { LinkCommand } from 'ember-command'; 8 | import sinon from 'sinon'; 9 | import FooBarLogCommand from 'test-app/components/command-demo/foobar-log-command'; 10 | import PushLogCommand from 'test-app/components/command-demo/push-log-command'; 11 | 12 | import { arrangeCommandInstance } from 'ember-command/test-support'; 13 | import { linkFor, setupLink } from 'ember-link/test-support'; 14 | 15 | import type { TestContext as BaseTestContext } from '@ember/test-helpers'; 16 | import type { CommandAction } from 'ember-command'; 17 | import type { TestLink } from 'ember-link/test-support'; 18 | import type { SinonSpy } from 'sinon'; 19 | 20 | interface TestContext extends BaseTestContext { 21 | command: CommandAction; 22 | link: TestLink; 23 | } 24 | 25 | module('Rendering | Component | ', function (hooks) { 26 | setupRenderingTest(hooks); 27 | setupLink(hooks); 28 | 29 | test('it renders "blank"', async function (this: TestContext, assert) { 30 | await render(hbs` 31 | 32 | `); 33 | 34 | assert.dom('[data-test-commander]').hasTagName('span'); 35 | assert.dom('[data-test-commander]').doesNotHaveAttribute('type'); 36 | }); 37 | 38 | test('it renders @element', async function (this: TestContext, assert) { 39 | await render(hbs` 40 | {{!@glint-expect-error}} 41 | 42 | `); 43 | 44 | assert.dom('[data-test-commander]').hasTagName('abbr'); 45 | assert.dom('[data-test-commander]').doesNotHaveAttribute('type'); 46 | }); 47 | 48 | test('it renders for a function', async function (this: TestContext, assert) { 49 | this.command = sinon.spy(); 50 | await render(hbs``); 51 | 52 | assert.dom('[data-test-commander]').hasTagName('button'); 53 | assert.dom('[data-test-commander]').hasAttribute('type'); 54 | assert.dom('[data-test-commander]').doesNotHaveAttribute('href'); 55 | }); 56 | 57 | test('it renders for a link', async function (this: TestContext, assert) { 58 | this.owner.register('route:test-route', class extends Route {}); 59 | 60 | const linkService = this.owner.lookup('service:link-manager'); 61 | 62 | this.command = linkService.createLink({ 63 | route: 'test-route' 64 | }); 65 | 66 | await render(hbs``); 67 | 68 | assert.dom('[data-test-commander]').hasTagName('a'); 69 | assert.dom('[data-test-commander]').doesNotHaveAttribute('type'); 70 | assert.dom('[data-test-commander]').hasAttribute('href'); 71 | 72 | this.link = linkFor('test-route'); 73 | await render(hbs``); 74 | assert.dom('[data-test-commander]').hasTagName('a'); 75 | assert.dom('[data-test-commander]').doesNotHaveAttribute('type'); 76 | assert.dom('[data-test-commander]').hasAttribute('href'); 77 | }); 78 | 79 | test('it renders for a command', async function (this: TestContext, assert) { 80 | this.command = arrangeCommandInstance(new PushLogCommand()); 81 | await render(hbs``); 82 | 83 | assert.dom('[data-test-commander]').hasTagName('button'); 84 | assert.dom('[data-test-commander]').hasAttribute('type'); 85 | assert.dom('[data-test-commander]').doesNotHaveAttribute('href'); 86 | }); 87 | 88 | test('it renders for a link command', async function (this: TestContext, assert) { 89 | this.owner.register('route:test-route', class extends Route {}); 90 | this.command = arrangeCommandInstance(new LinkCommand({ route: 'test-route' })); 91 | 92 | await render(hbs``); 93 | 94 | assert.dom('[data-test-commander]').hasTagName('a'); 95 | assert.dom('[data-test-commander]').hasAttribute('href'); 96 | }); 97 | 98 | // compounds 99 | 100 | test('it renders for compound command', async function (this: TestContext, assert) { 101 | this.command = arrangeCommandInstance([new PushLogCommand(), new FooBarLogCommand()]); 102 | await render(hbs``); 103 | 104 | assert.dom('[data-test-commander]').hasTagName('button'); 105 | assert.dom('[data-test-commander]').hasAttribute('type'); 106 | assert.dom('[data-test-commander]').doesNotHaveAttribute('href'); 107 | }); 108 | 109 | test('it renders for compound link', async function (this: TestContext, assert) { 110 | this.owner.register('route:test-route', class extends Route {}); 111 | this.owner.register('route:test-route2', class extends Route {}); 112 | this.command = arrangeCommandInstance([ 113 | new LinkCommand({ route: 'test-route' }), 114 | new LinkCommand({ route: 'test-route2' }) 115 | ]); 116 | await render(hbs``); 117 | 118 | assert.dom('[data-test-commander]').hasTagName('a'); 119 | assert.dom('[data-test-commander]').hasAttribute('href'); 120 | }); 121 | 122 | test('it renders for compound command + link', async function (this: TestContext, assert) { 123 | this.owner.register('route:test-route', class extends Route {}); 124 | this.command = arrangeCommandInstance([ 125 | new LinkCommand({ route: 'test-route' }), 126 | new FooBarLogCommand() 127 | ]); 128 | await render(hbs``); 129 | 130 | assert.dom('[data-test-commander]').hasTagName('a'); 131 | assert.dom('[data-test-commander]').hasAttribute('href'); 132 | 133 | this.command = arrangeCommandInstance([linkFor('test-route'), new FooBarLogCommand()]); 134 | await render(hbs``); 135 | 136 | assert.dom('[data-test-commander]').hasTagName('a'); 137 | assert.dom('[data-test-commander]').hasAttribute('href'); 138 | }); 139 | 140 | // invoke 141 | test('invoke function', async function (this: TestContext, assert) { 142 | this.command = sinon.spy(); 143 | await render(hbs``); 144 | 145 | await click('[data-test-commander]'); 146 | assert.ok((this.command as SinonSpy).calledOnce); 147 | 148 | this.command = arrangeCommandInstance([new PushLogCommand(), new FooBarLogCommand()]); 149 | await render(hbs``); 150 | 151 | assert.dom('[data-test-commander]').hasTagName('button'); 152 | assert.dom('[data-test-commander]').hasAttribute('type'); 153 | assert.dom('[data-test-commander]').doesNotHaveAttribute('href'); 154 | }); 155 | 156 | test('invoke command', async function (this: TestContext, assert) { 157 | const foo = new FooBarLogCommand(); 158 | const spy = sinon.spy(); 159 | 160 | foo.execute = spy; 161 | this.command = arrangeCommandInstance(foo); 162 | 163 | await render(hbs``); 164 | 165 | await click('[data-test-commander]'); 166 | assert.ok(spy.calledOnce); 167 | }); 168 | 169 | test('invoke link', async function (this: TestContext, assert) { 170 | this.link = linkFor('some.route'); 171 | 172 | this.link.onTransitionTo = () => { 173 | assert.step('link clicked'); 174 | }; 175 | 176 | await render(hbs``); 177 | 178 | await click('[data-test-commander]'); 179 | assert.verifySteps(['link clicked']); 180 | }); 181 | }); 182 | -------------------------------------------------------------------------------- /test-app/tests/rendering/mutation-test.gts: -------------------------------------------------------------------------------- 1 | import { tracked } from '@glimmer/tracking'; 2 | import { fn } from '@ember/helper'; 3 | import { on } from '@ember/modifier'; 4 | import { click, render } from '@ember/test-helpers'; 5 | import { module, test } from 'qunit'; 6 | import { setupRenderingTest } from 'ember-qunit'; 7 | 8 | import { command, commandFor } from 'ember-command'; 9 | import MutateAction from 'test-app/actions/mutate-action'; 10 | 11 | import { arrangeCommandInstance } from 'ember-command/test-support'; 12 | 13 | import TaskCommand from '../commands/task-command'; 14 | 15 | module('Rendering | mutation', function (hooks) { 16 | setupRenderingTest(hooks); 17 | 18 | test('plain invocation', async (assert) => { 19 | const obj = {}; 20 | const changeset = { foo: 'bar' }; 21 | const cmd = arrangeCommandInstance(new MutateAction()); 22 | 23 | await render( 24 | 27 | ); 28 | 29 | assert.notOk('foo' in obj); 30 | 31 | await click('button'); 32 | 33 | assert.ok('foo' in obj); 34 | }); 35 | 36 | test('change @tracked', async (assert) => { 37 | class Comp { 38 | @tracked carried = false; 39 | 40 | bag = { 41 | carry: false 42 | }; 43 | 44 | @command task = commandFor(new TaskCommand(this.bag)); 45 | 46 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 47 | // @ts-ignore 48 | @command run = commandFor(this.runTask); 49 | 50 | runTask = async () => { 51 | await this.task(); 52 | 53 | this.carried = this.bag.carry; 54 | }; 55 | } 56 | 57 | const comp = new Comp(); 58 | 59 | await render( 60 | 67 | ); 68 | 69 | assert.notOk(comp.bag.carry); 70 | assert.dom('span').doesNotExist(); 71 | 72 | await click('button'); 73 | 74 | assert.ok(comp.bag.carry); 75 | assert.dom('span').exists(); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /test-app/tests/test-helper.ts: -------------------------------------------------------------------------------- 1 | import { setApplication } from '@ember/test-helpers'; 2 | import * as QUnit from 'qunit'; 3 | import { setup } from 'qunit-dom'; 4 | import { start } from 'ember-qunit'; 5 | 6 | import setupSinon from 'ember-sinon-qunit'; 7 | import Application from 'test-app/app'; 8 | import config from 'test-app/config/environment'; 9 | 10 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 11 | // @ts-ignore 12 | setApplication(Application.create(config.APP)); 13 | 14 | setup(QUnit.assert); 15 | setupSinon(); 16 | 17 | start(); 18 | -------------------------------------------------------------------------------- /test-app/tests/unit/action-test.ts: -------------------------------------------------------------------------------- 1 | import Service from '@ember/service'; 2 | import { module, test } from 'qunit'; 3 | import { setupTest } from 'ember-qunit'; 4 | 5 | import { action } from 'ember-command'; 6 | 7 | import type { TestContext } from '@ember/test-helpers'; 8 | 9 | class MathService extends Service { 10 | // eslint-disable-next-line @typescript-eslint/naming-convention 11 | PI = Math.PI; 12 | 13 | add(a: number, b: number) { 14 | return a + b; 15 | } 16 | } 17 | 18 | declare module '@ember/service' { 19 | interface Registry { 20 | math: MathService; 21 | } 22 | } 23 | 24 | module('Unit | action()', function (hooks) { 25 | setupTest(hooks); 26 | 27 | hooks.beforeEach(function (this: TestContext) { 28 | this.owner.register('service:math', MathService); 29 | }); 30 | 31 | test('Access parameter free', function (this: TestContext, assert) { 32 | const gimmePie = action( 33 | ({ services }) => 34 | () => 35 | services.math.PI 36 | )(this.owner); 37 | 38 | assert.equal(gimmePie(), Math.PI); 39 | }); 40 | 41 | test('With parameters', function (this: TestContext, assert) { 42 | const add = action( 43 | ({ services }) => 44 | (a: number, b: number) => 45 | services.math.add(a, b) 46 | )(this.owner); 47 | 48 | assert.equal(add(3, 5), 8); 49 | }); 50 | 51 | test('Curried', function (this: TestContext, assert) { 52 | const add = action( 53 | ({ services }) => 54 | (a: number, b: number) => 55 | services.math.add(a, b) 56 | )(this.owner); 57 | 58 | const addOne = (b: number) => add(1, b); 59 | 60 | assert.equal(addOne(5), 6); 61 | }); 62 | 63 | test('Async', async function (this: TestContext, assert) { 64 | const gimmePie = action(({ services }) => async () => { 65 | await new Promise((resolve) => window.setTimeout(resolve, 0)); 66 | 67 | return services.math.PI; 68 | })(this.owner); 69 | 70 | const pi = await gimmePie(); 71 | 72 | assert.equal(pi, Math.PI); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /test-app/tests/unit/command-decorator-test.ts: -------------------------------------------------------------------------------- 1 | import { action } from '@ember/object'; 2 | import { module, test } from 'qunit'; 3 | import { setupTest } from 'ember-qunit'; 4 | 5 | import { command, commandFor } from 'ember-command'; 6 | import CounterDecrementCommand from 'test-app/components/command-demo/counter-decrement-command'; 7 | import CounterIncrementCommand from 'test-app/components/command-demo/counter-increment-command'; 8 | 9 | import { setOwner } from '../-owner'; 10 | 11 | class CommandAggregator { 12 | @command inc = commandFor(new CounterIncrementCommand()); 13 | 14 | @command 15 | get dec() { 16 | return commandFor(new CounterDecrementCommand()); 17 | } 18 | 19 | @command noop = undefined; 20 | 21 | @command 22 | get npe() { 23 | return undefined; 24 | } 25 | 26 | @action 27 | runInc() { 28 | void this.inc(); 29 | } 30 | 31 | runDec = () => { 32 | void this.dec(); 33 | }; 34 | } 35 | 36 | module('Unit | @command decorator', function (hooks) { 37 | setupTest(hooks); 38 | 39 | test('it works with property', function (assert) { 40 | const aggregator = new CommandAggregator(); 41 | 42 | // eslint-disable-next-line @typescript-eslint/no-invalid-this 43 | setOwner(aggregator, this.owner); 44 | 45 | assert.ok(aggregator.inc); 46 | }); 47 | 48 | test('it works with getter', function (assert) { 49 | const aggregator = new CommandAggregator(); 50 | 51 | // eslint-disable-next-line @typescript-eslint/no-invalid-this 52 | setOwner(aggregator, this.owner); 53 | 54 | assert.ok(aggregator.dec); 55 | }); 56 | 57 | test('it works with @action', function (assert) { 58 | // eslint-disable-next-line @typescript-eslint/no-invalid-this 59 | const counter = this.owner.lookup('service:counter'); 60 | const aggregator = new CommandAggregator(); 61 | 62 | // eslint-disable-next-line @typescript-eslint/no-invalid-this 63 | setOwner(aggregator, this.owner); 64 | 65 | assert.strictEqual(counter.counter, 0); 66 | aggregator.runInc(); 67 | 68 | assert.strictEqual(counter.counter, 1); 69 | }); 70 | 71 | test('it works with undefined property', function (assert) { 72 | const aggregator = new CommandAggregator(); 73 | 74 | // eslint-disable-next-line @typescript-eslint/no-invalid-this 75 | setOwner(aggregator, this.owner); 76 | 77 | assert.strictEqual(aggregator.noop, undefined); 78 | }); 79 | 80 | test('it works with undefined getter', function (assert) { 81 | const aggregator = new CommandAggregator(); 82 | 83 | // eslint-disable-next-line @typescript-eslint/no-invalid-this 84 | setOwner(aggregator, this.owner); 85 | 86 | assert.strictEqual(aggregator.npe, undefined); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /test-app/tests/unit/command-test.ts: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import { module, test } from 'qunit'; 3 | import { setupTest } from 'ember-qunit'; 4 | 5 | import { LinkCommand } from 'ember-command'; 6 | 7 | import { arrangeCommand, arrangeCommandInstance } from 'ember-command/test-support'; 8 | import { setupLink } from 'ember-link/test-support'; 9 | 10 | import type { TestContext } from '@ember/test-helpers'; 11 | 12 | module('Unit | Identify command instances', function (hooks) { 13 | setupTest(hooks); 14 | setupLink(hooks); 15 | 16 | test('a link is a link', function (this: TestContext, assert) { 17 | this.owner.register('route:test-route', class extends Route {}); 18 | 19 | const linkService = this.owner.lookup('service:link-manager'); 20 | 21 | const link = linkService.createLink({ 22 | route: 'test-route' 23 | }); 24 | 25 | const command = arrangeCommandInstance(link); 26 | 27 | assert.ok(command.link); 28 | }); 29 | 30 | /** 31 | * This tests only exists because of: 32 | * https://github.com/gossi/ember-command/issues/23 33 | */ 34 | test('a link command is a link', function (this: TestContext, assert) { 35 | this.owner.register('route:test-route', class extends Route {}); 36 | 37 | const command = arrangeCommand(new LinkCommand({ route: 'test-route' })); 38 | 39 | assert.ok(LinkCommand.isLinkCommand(command)); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/ember/tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "paths": { 6 | "test-app/tests/*": [ 7 | "tests/*" 8 | ], 9 | "test-app/*": [ 10 | "app/*" 11 | ], 12 | "*": [ 13 | "types/*" 14 | ] 15 | }, 16 | // make type checking work: 17 | "skipLibCheck": false, 18 | // allow tests to compile for ember versions below v3 19 | "noEmitOnError": false 20 | }, 21 | "include": [ 22 | "app/**/*", 23 | "tests/**/*", 24 | "types/**/*" 25 | ], 26 | "glint": { 27 | "environment": [ 28 | "ember-loose", 29 | "ember-template-imports" 30 | ] 31 | } 32 | } -------------------------------------------------------------------------------- /test-app/types/ember-sinon-qunit.d.ts: -------------------------------------------------------------------------------- 1 | export default function setupSinon(): void; 2 | -------------------------------------------------------------------------------- /test-app/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import 'ember-source/types'; 2 | import 'ember-source/types/preview'; 3 | import '@glint/environment-ember-loose'; 4 | import '@glint/environment-ember-template-imports'; 5 | 6 | import type EmberCommandRegistry from 'ember-command/template-registry'; 7 | import type EmberLinkRegistry from 'ember-link/template-registry'; 8 | 9 | declare module '@glint/environment-ember-loose/registry' { 10 | export default interface Registry extends EmberCommandRegistry, EmberLinkRegistry { 11 | // local entries 12 | } 13 | } 14 | --------------------------------------------------------------------------------