diff --git a/docs/extend/api.md b/docs/extend/api.md index cb03f7f0c..0f1ad5322 100644 --- a/docs/extend/api.md +++ b/docs/extend/api.md @@ -336,7 +336,7 @@ return [ (new Extend\ApiSerializer(UserSerializer::class)) // One attribute at a time ->attribute('firstName', function ($serializer, $user, $attributes) { - return $user->first_name + return $user->first_name }) // Multiple modifications at once, more complex logic ->mutate(function($serializer, $user, $attributes) { diff --git a/docs/extend/github-actions.md b/docs/extend/github-actions.md index 3179a4ec7..1051a2b13 100644 --- a/docs/extend/github-actions.md +++ b/docs/extend/github-actions.md @@ -4,8 +4,6 @@ In public repos, [GitHub Actions](https://github.com/features/actions) allow you In this guide, you will learn how to add pre-defined workflows to your extension. -## Setup - :::tip [Flarum CLI](https://github.com/flarum/cli) You can use the CLI to automatically add and update workflows to your code: @@ -15,6 +13,9 @@ $ flarum-cli infra githubActions ::: +## Backend + + All you need to do is create a `.github/workflows/backend.yml` file in your extension, it will reuse a predefined workflow by the core development team which can be found [here](https://github.com/flarum/framework/blob/main/.github/workflows/REUSABLE_backend.yml). You need to specify the configuration as follows: ```yaml @@ -34,9 +35,7 @@ jobs: backend_directory: . ``` -## Backend - -Flarum provides a pre-defined workflow for running certain jobs for the backend of your extension. These are the currently available jobs: +These are the currently available jobs: | Name | Key | Description | |-------------------------------------------------|--------------------------|----------------------------------------| @@ -69,4 +68,52 @@ For more details on parameters, [checkout the full predefined reusable workflow ## Frontend -Soon.. +All you need to do is create a `.github/workflows/frontend.yml` file in your extension, it will reuse a predefined workflow by the core development team which can be found [here](https://github.com/flarum/framework/blob/main/.github/workflows/REUSABLE_frontend.yml). You need to specify the configuration as follows: + +```yaml +name: Frontend + +on: [workflow_dispatch, push, pull_request] + +jobs: + run: + uses: flarum/framework/.github/workflows/REUSABLE_frontend.yml@main + with: + enable_bundlewatch: false + enable_prettier: true + enable_typescript: false + + frontend_directory: ./js + backend_directory: . + js_package_manager: yarn + main_git_branch: main + + secrets: + bundlewatch_github_token: ${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }} +``` + +Unlike the backend workflow, the frontend workflow runs everything in a single job. Here are the available parameters: + +| Name | Key | Description | Format | +|-----------------------|-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|--------| +| Build Script | `build_script` | Script to run for production build. Empty value to disable. | string | +| Build Typings Script | `build_typings_script` | Script to run for typings build. Empty value to disable. | string | +| Format Script | `format_script` | Script to run for code formatting. Empty value to disable. | string | +| Check Typings Script | `check_typings_script` | Script to run for tyiping check. Empty value to disable. | string | +| Type Coverage Script | `type_coverage_script` | Script to run for type coverage. Empty value to disable. | string | +| Test Script | `test_script` | Script to run for tests. Empty value to disable. | string | +| Enable Bundlewatch | `enable_bundlewatch` | Enable Bundlewatch? | string | +| Enable Prettier | `enable_prettier` | Enable Prettier? | string | +| Enable Typescript | `enable_typescript` | Enable TypeScript? | string | +| Enable Tests | `enable_tests` | Enable Tests? | string | +| Backend Directory | `backend_directory` | The directory of the project where backend code is located. This should contain a `composer.json` file, and is generally the root directory of the repo. | string | +| Frontend Directory | `frontend_directory` | The directory of the project where frontend code is located. This should contain a `package.json` file. | string | +| Main Git Branch | `main_git_branch` | The main git branch to use for the workflow. | string | +| Node Version | `node_version` | The node version to use for the workflow. | string | +| JS Package Manager | `js_package_manager` | The package manager to use (ex. yarn) | string | +| Cache Dependency Path | `cache_dependency_path` | The path to the cache dependency file. | string | +:::tip + +For more details on parameters, [checkout the full predefined reusable workflow file](https://github.com/flarum/framework/blob/main/.github/workflows/REUSABLE_frontend.yml). + +::: diff --git a/docs/extend/models.md b/docs/extend/models.md index 007f0e693..31cd4bee1 100644 --- a/docs/extend/models.md +++ b/docs/extend/models.md @@ -251,38 +251,37 @@ export default class Tag extends Model { } ``` -You must then register your new model with the store: +You must then register your new model with the store using the frontend `Store` extender in a new `extend.js` module: ```js -app.store.models.tags = Tag; -``` - - +``` -### Extending Models +:::info -To add attributes and relationships to existing models, modify the model class prototype: +Remember to export the `extend` module from your entry `index.js` file: ```js -Discussion.prototype.user = Model.hasOne('user'); -Discussion.prototype.posts = Model.hasMany('posts'); -Discussion.prototype.slug = Model.attribute('slug'); +export { default as extend } from './extend'; ``` - +### Extending Models + +To add attributes and relationships to existing models, use the `Model` extender: + +```ts + new Extend.Model(Discussion) + .attribute('slug') + .hasOne('user') + .hasMany('posts') +``` ### Saving Resources diff --git a/docs/extend/routes.md b/docs/extend/routes.md index 12997bbae..1e83b1f32 100644 --- a/docs/extend/routes.md +++ b/docs/extend/routes.md @@ -160,20 +160,28 @@ On the backend, instead of adding your frontend route via the `Routes` extender, Now when `yourforum.com/users` is visited, the forum frontend will be displayed. However, since the frontend doesn't yet know about the `users` route, the discussion list will still be rendered. -Flarum builds on [Mithril's routing system](https://mithril.js.org/index.html#routing), adding route names and an abstract class for pages (`common/components/Page`). To register a new route, add an object for it to `app.routes`: +Flarum builds on [Mithril's routing system](https://mithril.js.org/index.html#routing), adding route names and an abstract class for pages (`common/components/Page`). -```js -app.routes['acme.users'] = { path: '/users', component: UsersPage }; -``` - - +``` + +:::info + +Remember to export the `extend` module from your entry `index.js` file: + +```js +export { default as extend } from './extend'; +``` + +::: Now when `yourforum.com/users` is visited, the forum frontend will be loaded and the `UsersPage` component will be rendered in the content area. For more information on frontend pages, please see [that documentation section](frontend-pages.md). @@ -181,16 +189,12 @@ Advanced use cases might also be interested in using [route resolvers](frontend- ### Route Parameters -Frontend routes also allow you to capture segments of the URI, but the [Mithril route syntax](https://mithril.js.org/route.html) is slightly different: +Frontend routes also allow you to capture segments of the URI: ```jsx -app.routes['acme.user'] = { path: '/user/:id', component: UserPage }; -``` - - + .add('acme.user', '/user/:id', ) +``` Route parameters will be passed into the `attrs` of the route's component. They will also be available through [`m.route.param`](https://mithril.js.org/route.html#mrouteparam) @@ -203,6 +207,21 @@ const url = app.route('acme.user', { id: 123, foo: 'bar' }); // http://yourforum.com/users/123?foo=bar ``` +The extender also allows you to define a route helper method: + +```js + new Extend.Routes() + .add('acme.user', '/user/:id', ) + .helper('acmeUser', (user) => app.route('acme.user', { id: user.id() })) +``` + +This allows you to generate URLs to the route using the `acmeUser` helper method: + +```js +const url = app.route.acmeUser(user); +// http://yourforum.com/users/123 +``` + ### Linking to Other Pages A forum wouldn't be very useful if it only had one page. diff --git a/docs/extend/testing.md b/docs/extend/testing.md index f38b90fbf..6a9b32402 100644 --- a/docs/extend/testing.md +++ b/docs/extend/testing.md @@ -20,8 +20,6 @@ $ flarum-cli infra backendTesting ::: -Firstly, you will need to require the `flarum/testing` composer package as a dev dependency for your extension: - ```bash composer require --dev flarum/testing:^1.0 ``` @@ -405,7 +403,167 @@ NOTE: If you find your extension needs _lots and lots_ of mocks, or mocks that f ## Frontend Tests -Coming Soon! +### Setup + +:::tip [Flarum CLI](https://github.com/flarum/cli) + +You can use the CLI to automatically add and update frontend testing infrastructure to your code: + +```bash +$ flarum-cli infra frontendTesting +``` + +::: + +First, you need to install the Jest config dev dependency: + +```bash +$ yarn add --dev @flarum/jest-config +``` + +Then, add the following to your `package.json`: + +```json +{ + "type": "module", + "scripts": { + ..., + "test": "yarn node --experimental-vm-modules $(yarn bin jest)" + } +} +``` + +Rename `webpack.config.js` to `webpack.config.cjs`. This is necessary because Jest doesn't support ESM yet. + +Create a `jest.config.cjs` file in the root of your extension: + +```js +module.exports = require('@flarum/jest-config')(); +``` + +If you are using TypeScript, create tsconfig.test.json with the following content: + +```json +{ + "extends": "./tsconfig.json", + "include": ["tests/**/*"], + "files": ["../../../node_modules/@flarum/jest-config/shims.d.ts"] +} +``` + +Then, you will need to set up a file structure for tests: + +``` +js +├── dist +├── src +├── tests +│ ├── unit +│ │ └── functionTest.test.js +│ ├── integration +│ │ └── componentTest.test.js +├── package.json +├── tsconfig.json +├── tsconfig.test.json +├── jest.config.cjs +└── webpack.config.cjs +``` + +#### GitHub Testing Workflow + +To run tests on every commit and pull request, check out the [GitHub Actions](github-actions.md) page. + +### Using Unit Tests + +Like any other JS project, you can use Jest to write unit tests for your frontend code. Checkout the [Jest docs](https://jestjs.io/docs/using-matchers) for more information on how to write tests. + +Here's a simple example of a unit test fo core's `abbreviateNumber` function: + +```ts +import abbreviateNumber from '../../../../src/common/utils/abbreviateNumber'; + +test('does not change small numbers', () => { + expect(abbreviateNumber(1)).toBe('1'); +}); + +test('abbreviates large numbers', () => { + expect(abbreviateNumber(1000000)).toBe('1M'); + expect(abbreviateNumber(100500)).toBe('100.5K'); +}); + +test('abbreviates large numbers with decimal places', () => { + expect(abbreviateNumber(100500)).toBe('100.5K'); + expect(abbreviateNumber(13234)).toBe('13.2K'); +}); +``` + +### Using Integration Tests + +Integration tests are used to test the components of your frontend code and the interaction between different components. For example, you might test that a page component renders the correct content based on certain parameters. + +Here's a simple example of an integration test for core's `Alert` component: + +```ts +import Alert from '../../../../src/common/components/Alert'; +import m from 'mithril'; +import mq from 'mithril-query'; +import { jest } from '@jest/globals'; + +describe('Alert displays as expected', () => { + it('should display alert messages with an icon', () => { + const alert = mq(m(Alert, { type: 'error' }, 'Shoot!')); + expect(alert).toContainRaw('Shoot!'); + expect(alert).toHaveElement('i.icon'); + }); + + it('should display alert messages with a custom icon when using a title', () => { + const alert = mq(Alert, { type: 'error', icon: 'fas fa-users', title: 'Woops..' }); + expect(alert).toContainRaw('Woops..'); + expect(alert).toHaveElement('i.fas.fa-users'); + }); + + it('should display alert messages with a title', () => { + const alert = mq(m(Alert, { type: 'error', title: 'Error Title' }, 'Shoot!')); + expect(alert).toContainRaw('Shoot!'); + expect(alert).toContainRaw('Error Title'); + expect(alert).toHaveElement('.Alert-title'); + }); + + it('should display alert messages with custom controls', () => { + const alert = mq(Alert, { type: 'error', controls: [m('button', { className: 'Button--test' }, 'Click me!')] }); + expect(alert).toHaveElement('button.Button--test'); + }); +}); + +describe('Alert is dismissible', () => { + it('should show dismiss button', function () { + const alert = mq(m(Alert, { dismissible: true }, 'Shoot!')); + expect(alert).toHaveElement('button.Alert-dismiss'); + }); + + it('should call ondismiss when dismiss button is clicked', function () { + const ondismiss = jest.fn(); + const alert = mq(Alert, { dismissible: true, ondismiss }); + alert.click('.Alert-dismiss'); + expect(ondismiss).toHaveBeenCalled(); + }); + + it('should not be dismissible if not chosen', function () { + const alert = mq(Alert, { type: 'error', dismissible: false }); + expect(alert).not.toHaveElement('button.Alert-dismiss'); + }); +}); +``` + +#### Methods + +These are the custom methods that are available for mithril component tests: +* **`toHaveElement(selector)`** - Checks if the component has an element that matches the given selector. +* **`toContainRaw(content)`** - Checks if the component HTML contains the given content. + +To negate any of these methods, simply prefix them with `not.`. For example, `expect(alert).not.toHaveElement('button.Alert-dismiss');`. For more information, check out the [Jest docs](https://jestjs.io/docs/using-matchers). For example you may need to check how to [mock functions](https://jestjs.io/docs/mock-functions), or how to use `beforeEach` and `afterEach` to set up and tear down tests. + + ## E2E Tests diff --git a/i18n/de/docusaurus-plugin-content-docs/current/extend/api.md b/i18n/de/docusaurus-plugin-content-docs/current/extend/api.md index 0ad440f0e..073d858e1 100644 --- a/i18n/de/docusaurus-plugin-content-docs/current/extend/api.md +++ b/i18n/de/docusaurus-plugin-content-docs/current/extend/api.md @@ -331,7 +331,7 @@ return [ (new Extend\ApiSerializer(UserSerializer::class)) // One attribute at a time ->attribute('firstName', function ($serializer, $user, $attributes) { - return $user->first_name + return $user->first_name }) // Multiple modifications at once, more complex logic ->mutate(function($serializer, $user, $attributes) { diff --git a/i18n/de/docusaurus-plugin-content-docs/current/extend/github-actions.md b/i18n/de/docusaurus-plugin-content-docs/current/extend/github-actions.md index d810720cd..904bdf995 100644 --- a/i18n/de/docusaurus-plugin-content-docs/current/extend/github-actions.md +++ b/i18n/de/docusaurus-plugin-content-docs/current/extend/github-actions.md @@ -4,8 +4,6 @@ In public repos, [GitHub Actions](https://github.com/features/actions) allow you In this guide, you will learn how to add pre-defined workflows to your extension. -## Setup - :::tip [Flarum CLI](https://github.com/flarum/cli) You can use the CLI to automatically add and update workflows to your code: @@ -15,6 +13,9 @@ $ flarum-cli infra githubActions ::: +## Backend + + All you need to do is create a `.github/workflows/backend.yml` file in your extension, it will reuse a predefined workflow by the core development team which can be found [here](https://github.com/flarum/framework/blob/main/.github/workflows/REUSABLE_backend.yml). You need to specify the configuration as follows: ```yaml @@ -34,9 +35,7 @@ jobs: backend_directory: . ``` -## Backend - -Flarum provides a pre-defined workflow for running certain jobs for the backend of your extension. These are the currently available jobs: +These are the currently available jobs: | Name | Key | Description | | ----------------------------------------------- | ------------------------ | -------------------------------------- | @@ -69,4 +68,52 @@ For more details on parameters, [checkout the full predefined reusable workflow ## Frontend -Soon.. +All you need to do is create a `.github/workflows/frontend.yml` file in your extension, it will reuse a predefined workflow by the core development team which can be found [here](https://github.com/flarum/framework/blob/main/.github/workflows/REUSABLE_frontend.yml). You need to specify the configuration as follows: + +```yaml +name: Frontend + +on: [workflow_dispatch, push, pull_request] + +jobs: + run: + uses: flarum/framework/.github/workflows/REUSABLE_frontend.yml@main + with: + enable_bundlewatch: false + enable_prettier: true + enable_typescript: false + + frontend_directory: ./js + backend_directory: . + js_package_manager: yarn + main_git_branch: main + + secrets: + bundlewatch_github_token: ${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }} +``` + +Unlike the backend workflow, the frontend workflow runs everything in a single job. Here are the available parameters: + +| Name | Key | Description | Format | +| --------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| Build Script | `build_script` | Script to run for production build. Empty value to disable. | string | +| Build Typings Script | `build_typings_script` | Script to run for typings build. Empty value to disable. | string | +| Format Script | `format_script` | Script to run for code formatting. Empty value to disable. | string | +| Check Typings Script | `check_typings_script` | Script to run for tyiping check. Empty value to disable. | string | +| Type Coverage Script | `type_coverage_script` | Script to run for type coverage. Empty value to disable. | string | +| Test Script | `test_script` | Script to run for tests. Empty value to disable. | string | +| Enable Bundlewatch | `enable_bundlewatch` | Enable Bundlewatch? | string | +| Enable Prettier | `enable_prettier` | Enable Prettier? | string | +| Enable Typescript | `enable_typescript` | Enable TypeScript? | string | +| Enable Tests | `enable_tests` | Enable Tests? | string | +| Backend Directory | `backend_directory` | The directory of the project where backend code is located. This should contain a `composer.json` file, and is generally the root directory of the repo. | string | +| Frontend Directory | `frontend_directory` | The directory of the project where frontend code is located. This should contain a `package.json` file. | string | +| Main Git Branch | `main_git_branch` | The main git branch to use for the workflow. | string | +| Node Version | `node_version` | The node version to use for the workflow. | string | +| JS Package Manager | `js_package_manager` | The package manager to use (ex. yarn) | string | +| Cache Dependency Path | `cache_dependency_path` | The path to the cache dependency file. | string | + :::tip + +For more details on parameters, [checkout the full predefined reusable workflow file](https://github.com/flarum/framework/blob/main/.github/workflows/REUSABLE_frontend.yml). + +::: diff --git a/i18n/de/docusaurus-plugin-content-docs/current/extend/models.md b/i18n/de/docusaurus-plugin-content-docs/current/extend/models.md index 822f354f3..5fecd6926 100644 --- a/i18n/de/docusaurus-plugin-content-docs/current/extend/models.md +++ b/i18n/de/docusaurus-plugin-content-docs/current/extend/models.md @@ -252,39 +252,40 @@ export default class Tag extends Model { } ``` -You must then register your new model with the store: +You must then register your new model with the store using the frontend `Store` extender in a new `extend.js` module: ```js -app.store.models.tags = Tag; +import Extend from 'flarum/common/extenders'; + +export default [ + new Extend.Store() + .add('tags', Tag), +]; ``` +:::info - -### Extending Models -To add attributes and relationships to existing models, modify the model class prototype: - -```js -Discussion.prototype.user = Model.hasOne('user'); -Discussion.prototype.posts = Model.hasMany('posts'); -Discussion.prototype.slug = Model.attribute('slug'); +export { default as extend } from './extend'; ``` +::: - ### Saving Resources + To send data back through the API, call the `save` method on a model instance. This method returns a Promise which resolves with the same model instance: ```js diff --git a/i18n/de/docusaurus-plugin-content-docs/current/extend/routes.md b/i18n/de/docusaurus-plugin-content-docs/current/extend/routes.md index 2644c7bd2..b4f2102e4 100644 --- a/i18n/de/docusaurus-plugin-content-docs/current/extend/routes.md +++ b/i18n/de/docusaurus-plugin-content-docs/current/extend/routes.md @@ -160,37 +160,46 @@ On the backend, instead of adding your frontend route via the `Routes` extender, Now when `yourforum.com/users` is visited, the forum frontend will be displayed. However, since the frontend doesn't yet know about the `users` route, the discussion list will still be rendered. -Flarum builds on [Mithril's routing system](https://mithril.js.org/index.html#routing), adding route names and an abstract class for pages (`common/components/Page`). To register a new route, add an object for it to `app.routes`: +Flarum builds on [Mithril's routing system](https://mithril.js.org/index.html#routing), adding route names and an abstract class for pages (`common/components/Page`). -```js -app.routes['acme.users'] = { path: '/users', component: UsersPage }; -``` - - - +``` + +:::info + +Remember to export the `extend` module from your entry `index.js` file: + +```js +export { default as extend } from './extend'; +``` + +::: + Now when `yourforum.com/users` is visited, the forum frontend will be loaded and the `UsersPage` component will be rendered in the content area. For more information on frontend pages, please see [that documentation section](frontend-pages.md). + Advanced use cases might also be interested in using [route resolvers](frontend-pages.md#route-resolvers-advanced). + ### Route Parameters -Frontend routes also allow you to capture segments of the URI, but the [Mithril route syntax](https://mithril.js.org/route.html) is slightly different: + +Frontend routes also allow you to capture segments of the URI: ```jsx -app.routes['acme.user'] = { path: '/user/:id', component: UserPage }; + new Extend.Routes() + .add('acme.user', '/user/:id', ) ``` - - Route parameters will be passed into the `attrs` of the route's component. They will also be available through [`m.route.param`](https://mithril.js.org/route.html#mrouteparam) + ### Generating URLs + To generate a URL to a route on the frontend, use the `app.route` method. This accepts two arguments: the route name, and a hash of parameters. Parameters will fill in matching URI segments, otherwise they will be appended as query params. ```js @@ -198,6 +207,21 @@ const url = app.route('acme.user', { id: 123, foo: 'bar' }); // http://yourforum.com/users/123?foo=bar ``` +The extender also allows you to define a route helper method: + +```js + new Extend.Routes() + .add('acme.user', '/user/:id', ) + .helper('acmeUser', (user) => app.route('acme.user', { id: user.id() })) +``` + +This allows you to generate URLs to the route using the `acmeUser` helper method: + +```js +const url = app.route.acmeUser(user); +// http://yourforum.com/users/123 +``` + ### Linking to Other Pages A forum wouldn't be very useful if it only had one page. While you could, of course, implement links to other parts of your forum with HTML anchor tags and hardcoded links, this can be difficult to maintain, and defeats the purpose of Flarum being a [Single Page Application](https://en.wikipedia.org/wiki/Single-page_application) in the first place. diff --git a/i18n/de/docusaurus-plugin-content-docs/current/extend/testing.md b/i18n/de/docusaurus-plugin-content-docs/current/extend/testing.md index b09037be8..2607b03da 100644 --- a/i18n/de/docusaurus-plugin-content-docs/current/extend/testing.md +++ b/i18n/de/docusaurus-plugin-content-docs/current/extend/testing.md @@ -18,8 +18,6 @@ $ flarum-cli infra backendTesting ::: -Firstly, you will need to require the `flarum/testing` composer package as a dev dependency for your extension: - ```bash composer require --dev flarum/testing:^1.0 ``` @@ -400,7 +398,167 @@ NOTE: If you find your extension needs _lots and lots_ of mocks, or mocks that f ## Frontend Tests -Coming Soon! +### Setup + +:::tip [Flarum CLI](https://github.com/flarum/cli) + +You can use the CLI to automatically add and update frontend testing infrastructure to your code: + +```bash +$ flarum-cli infra frontendTesting +``` + +::: + +First, you need to install the Jest config dev dependency: + +```bash +$ yarn add --dev @flarum/jest-config +``` + +Then, add the following to your `package.json`: + +```json +{ + "type": "module", + "scripts": { + ..., + "test": "yarn node --experimental-vm-modules $(yarn bin jest)" + } +} +``` + +Rename `webpack.config.js` to `webpack.config.cjs`. This is necessary because Jest doesn't support ESM yet. + +Create a `jest.config.cjs` file in the root of your extension: + +```js +module.exports = require('@flarum/jest-config')(); +``` + +If you are using TypeScript, create tsconfig.test.json with the following content: + +```json +{ + "extends": "./tsconfig.json", + "include": ["tests/**/*"], + "files": ["../../../node_modules/@flarum/jest-config/shims.d.ts"] +} +``` + +Then, you will need to set up a file structure for tests: + +``` +js +├── dist +├── src +├── tests +│ ├── unit +│ │ └── functionTest.test.js +│ ├── integration +│ │ └── componentTest.test.js +├── package.json +├── tsconfig.json +├── tsconfig.test.json +├── jest.config.cjs +└── webpack.config.cjs +``` + +#### GitHub Testing Workflow + +To run tests on every commit and pull request, check out the [GitHub Actions](github-actions.md) page. + +### Using Unit Tests + +Like any other JS project, you can use Jest to write unit tests for your frontend code. Checkout the [Jest docs](https://jestjs.io/docs/using-matchers) for more information on how to write tests. + +Here's a simple example of a unit test fo core's `abbreviateNumber` function: + +```ts +import abbreviateNumber from '../../../../src/common/utils/abbreviateNumber'; + +test('does not change small numbers', () => { + expect(abbreviateNumber(1)).toBe('1'); +}); + +test('abbreviates large numbers', () => { + expect(abbreviateNumber(1000000)).toBe('1M'); + expect(abbreviateNumber(100500)).toBe('100.5K'); +}); + +test('abbreviates large numbers with decimal places', () => { + expect(abbreviateNumber(100500)).toBe('100.5K'); + expect(abbreviateNumber(13234)).toBe('13.2K'); +}); +``` + +### Using Integration Tests + +Integration tests are used to test the components of your frontend code and the interaction between different components. For example, you might test that a page component renders the correct content based on certain parameters. + +Here's a simple example of an integration test for core's `Alert` component: + +```ts +import Alert from '../../../../src/common/components/Alert'; +import m from 'mithril'; +import mq from 'mithril-query'; +import { jest } from '@jest/globals'; + +describe('Alert displays as expected', () => { + it('should display alert messages with an icon', () => { + const alert = mq(m(Alert, { type: 'error' }, 'Shoot!')); + expect(alert).toContainRaw('Shoot!'); + expect(alert).toHaveElement('i.icon'); + }); + + it('should display alert messages with a custom icon when using a title', () => { + const alert = mq(Alert, { type: 'error', icon: 'fas fa-users', title: 'Woops..' }); + expect(alert).toContainRaw('Woops..'); + expect(alert).toHaveElement('i.fas.fa-users'); + }); + + it('should display alert messages with a title', () => { + const alert = mq(m(Alert, { type: 'error', title: 'Error Title' }, 'Shoot!')); + expect(alert).toContainRaw('Shoot!'); + expect(alert).toContainRaw('Error Title'); + expect(alert).toHaveElement('.Alert-title'); + }); + + it('should display alert messages with custom controls', () => { + const alert = mq(Alert, { type: 'error', controls: [m('button', { className: 'Button--test' }, 'Click me!')] }); + expect(alert).toHaveElement('button.Button--test'); + }); +}); + +describe('Alert is dismissible', () => { + it('should show dismiss button', function () { + const alert = mq(m(Alert, { dismissible: true }, 'Shoot!')); + expect(alert).toHaveElement('button.Alert-dismiss'); + }); + + it('should call ondismiss when dismiss button is clicked', function () { + const ondismiss = jest.fn(); + const alert = mq(Alert, { dismissible: true, ondismiss }); + alert.click('.Alert-dismiss'); + expect(ondismiss).toHaveBeenCalled(); + }); + + it('should not be dismissible if not chosen', function () { + const alert = mq(Alert, { type: 'error', dismissible: false }); + expect(alert).not.toHaveElement('button.Alert-dismiss'); + }); +}); +``` + +#### Methods + +These are the custom methods that are available for mithril component tests: +* **`toHaveElement(selector)`** - Checks if the component has an element that matches the given selector. +* **`toContainRaw(content)`** - Checks if the component HTML contains the given content. + +To negate any of these methods, simply prefix them with `not.`. For example, `expect(alert).not.toHaveElement('button.Alert-dismiss');`. For more information, check out the [Jest docs](https://jestjs.io/docs/using-matchers). For example you may need to check how to [mock functions](https://jestjs.io/docs/mock-functions), or how to use `beforeEach` and `afterEach` to set up and tear down tests. + + ## E2E Tests diff --git a/i18n/es/docusaurus-plugin-content-docs/current/extend/api.md b/i18n/es/docusaurus-plugin-content-docs/current/extend/api.md index 9e51fe470..06d35f1cc 100644 --- a/i18n/es/docusaurus-plugin-content-docs/current/extend/api.md +++ b/i18n/es/docusaurus-plugin-content-docs/current/extend/api.md @@ -331,7 +331,7 @@ return [ (new Extend\ApiSerializer(UserSerializer::class)) // One attribute at a time ->attribute('firstName', function ($serializer, $user, $attributes) { - return $user->first_name + return $user->first_name }) // Multiple modifications at once, more complex logic ->mutate(function($serializer, $user, $attributes) { diff --git a/i18n/es/docusaurus-plugin-content-docs/current/extend/github-actions.md b/i18n/es/docusaurus-plugin-content-docs/current/extend/github-actions.md index 2303a07d3..0ed7f6dd9 100644 --- a/i18n/es/docusaurus-plugin-content-docs/current/extend/github-actions.md +++ b/i18n/es/docusaurus-plugin-content-docs/current/extend/github-actions.md @@ -4,8 +4,6 @@ In public repos, [GitHub Actions](https://github.com/features/actions) allow you In this guide, you will learn how to add pre-defined workflows to your extension. -## Configuración - :::tip [Flarum CLI](https://github.com/flarum/cli) You can use the CLI to automatically add and update workflows to your code: @@ -15,6 +13,9 @@ $ flarum-cli infra githubActions ::: +## Backend + + All you need to do is create a `.github/workflows/backend.yml` file in your extension, it will reuse a predefined workflow by the core development team which can be found [here](https://github.com/flarum/framework/blob/main/.github/workflows/REUSABLE_backend.yml). You need to specify the configuration as follows: ```yaml @@ -34,9 +35,7 @@ jobs: backend_directory: . ``` -## Backend - -Flarum provides a pre-defined workflow for running certain jobs for the backend of your extension. These are the currently available jobs: +These are the currently available jobs: | Name | Key | Descripción | | ----------------------------------------------- | ------------------------ | -------------------------------------- | @@ -69,4 +68,52 @@ For more details on parameters, [checkout the full predefined reusable workflow ## Frontend -Soon.. +All you need to do is create a `.github/workflows/frontend.yml` file in your extension, it will reuse a predefined workflow by the core development team which can be found [here](https://github.com/flarum/framework/blob/main/.github/workflows/REUSABLE_frontend.yml). You need to specify the configuration as follows: + +```yaml +name: Frontend + +on: [workflow_dispatch, push, pull_request] + +jobs: + run: + uses: flarum/framework/.github/workflows/REUSABLE_frontend.yml@main + with: + enable_bundlewatch: false + enable_prettier: true + enable_typescript: false + + frontend_directory: ./js + backend_directory: . + js_package_manager: yarn + main_git_branch: main + + secrets: + bundlewatch_github_token: ${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }} +``` + +Unlike the backend workflow, the frontend workflow runs everything in a single job. Here are the available parameters: + +| Name | Key | Descripción | Format | +| --------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| Build Script | `build_script` | Script to run for production build. Empty value to disable. | string | +| Build Typings Script | `build_typings_script` | Script to run for typings build. Empty value to disable. | string | +| Format Script | `format_script` | Script to run for code formatting. Empty value to disable. | string | +| Check Typings Script | `check_typings_script` | Script to run for tyiping check. Empty value to disable. | string | +| Type Coverage Script | `type_coverage_script` | Script to run for type coverage. Empty value to disable. | string | +| Test Script | `test_script` | Script to run for tests. Empty value to disable. | string | +| Enable Bundlewatch | `enable_bundlewatch` | Enable Bundlewatch? | string | +| Enable Prettier | `enable_prettier` | Enable Prettier? | string | +| Enable Typescript | `enable_typescript` | Enable TypeScript? | string | +| Enable Tests | `enable_tests` | Enable Tests? | string | +| Backend Directory | `backend_directory` | The directory of the project where backend code is located. This should contain a `composer.json` file, and is generally the root directory of the repo. | string | +| Frontend Directory | `frontend_directory` | The directory of the project where frontend code is located. This should contain a `package.json` file. | string | +| Main Git Branch | `main_git_branch` | The main git branch to use for the workflow. | string | +| Node Version | `node_version` | The node version to use for the workflow. | string | +| JS Package Manager | `js_package_manager` | The package manager to use (ex. yarn) | string | +| Cache Dependency Path | `cache_dependency_path` | The path to the cache dependency file. | string | + :::tip + +For more details on parameters, [checkout the full predefined reusable workflow file](https://github.com/flarum/framework/blob/main/.github/workflows/REUSABLE_frontend.yml). + +::: diff --git a/i18n/es/docusaurus-plugin-content-docs/current/extend/models.md b/i18n/es/docusaurus-plugin-content-docs/current/extend/models.md index 5d09f2b12..e2ede462c 100644 --- a/i18n/es/docusaurus-plugin-content-docs/current/extend/models.md +++ b/i18n/es/docusaurus-plugin-content-docs/current/extend/models.md @@ -252,39 +252,40 @@ export default class Tag extends Model { } ``` -You must then register your new model with the store: +You must then register your new model with the store using the frontend `Store` extender in a new `extend.js` module: ```js -app.store.models.tags = Tag; +import Extend from 'flarum/common/extenders'; + +export default [ + new Extend.Store() + .add('tags', Tag), +]; ``` +:::info - -### Extending Models -To add attributes and relationships to existing models, modify the model class prototype: - -```js -Discussion.prototype.user = Model.hasOne('user'); -Discussion.prototype.posts = Model.hasMany('posts'); -Discussion.prototype.slug = Model.attribute('slug'); +export { default as extend } from './extend'; ``` +::: - ### Saving Resources + To send data back through the API, call the `save` method on a model instance. This method returns a Promise which resolves with the same model instance: ```js diff --git a/i18n/es/docusaurus-plugin-content-docs/current/extend/routes.md b/i18n/es/docusaurus-plugin-content-docs/current/extend/routes.md index ee0508ce0..d14c1a651 100644 --- a/i18n/es/docusaurus-plugin-content-docs/current/extend/routes.md +++ b/i18n/es/docusaurus-plugin-content-docs/current/extend/routes.md @@ -145,7 +145,7 @@ class HelloWorldController implements RequestHandlerInterface ### Controladores API -El espacio de nombres `Flarum\Api\Controller` contiene una serie de clases abstractas de controladores que puedes extender para implementar fácilmente nuevos recursos JSON-API. Consulte [Working with Data](data.md) para obtener más información. +El espacio de nombres `Flarum\Api\Controller` contiene una serie de clases abstractas de controladores que puedes extender para implementar fácilmente nuevos recursos JSON-API. Consulte [Working with Data](/extend/data.md) para obtener más información. ## Rutas en el frontend @@ -160,62 +160,73 @@ En el backend, en lugar de añadir tu ruta del frontend a través del extensor ` Ahora, cuando se visite `suforo.com/usuarios`, se mostrará el frontend del foro. Sin embargo, dado que el frontend no conoce todavía la ruta `users`, la lista de discusión se seguirá mostrando. -Flarum se basa en el [sistema de rutas de Mithril](https://mithril.js.org/index.html#routing), añadiendo nombres de rutas y una clase abstracta para páginas (`common/components/Page`). Para registrar una nueva ruta, añade un objeto para ella a `app.routes`: +Flarum builds on [Mithril's routing system](https://mithril.js.org/index.html#routing), adding route names and an abstract class for pages (`common/components/Page`). + +To register the route on the frontend, there is a `Routes` extender which works much like the backend one. Instead of a controller, however, you pass a component instance as the third argument: + +```jsx +import Extend from 'flarum/common/extenders'; + +export default [ + new Extend.Routes() + .add('acme.users', '/users', ), +]; +``` + +:::info + +Remember to export the `extend` module from your entry `index.js` file: ```js -app.routes['acme.users'] = { path: '/users', component: UsersPage }; +export { default as extend } from './extend'; ``` +::: - -Ahora, cuando se visite `suforo.com/usuarios`, se cargará el frontend del foro y se mostrará el componente `UsersPage` en el área de contenido. Para más información sobre las páginas del frontend, por favor vea [esa sección de documentación](frontend-pages.md). -Los casos de uso avanzados también pueden estar interesados en utilizar [route resolvers](frontend-pages.md#route-resolvers-advanced). ### Parámetros de ruta -Las rutas frontales también permiten capturar segmentos del URI, pero la [sintaxis de la ruta Mithril](https://mithril.js.org/route.html) es ligeramente diferente: + +Frontend routes also allow you to capture segments of the URI: ```jsx -app.routes['acme.user'] = { path: '/user/:id', component: UserPage }; + new Extend.Routes() + .add('acme.user', '/user/:id', ) ``` +Route parameters will be passed into the `attrs` of the route's component. They will also be available through [`m.route.param`](https://mithril.js.org/route.html#mrouteparam) - -Los parámetros de la ruta se pasarán a los `attrs` del componente de la ruta. También estarán disponibles a través de [`m.route.param`](https://mithril.js.org/route.html#mrouteparam) ### Generación de URLs -Para generar una URL a una ruta en el frontend, utilice el método `app.route`. Este método acepta dos argumentos: el nombre de la ruta y un hash de parámetros. Los parámetros rellenarán los segmentos de URI que coincidan, de lo contrario se añadirán como parámetros de consulta. + +To generate a URL to a route on the frontend, use the `app.route` method. This accepts two arguments: the route name, and a hash of parameters. Los parámetros rellenarán los segmentos de URI que coincidan, de lo contrario se añadirán como parámetros de consulta. ```js -import Link from 'flarum/components/Link'; +const url = app.route('acme.user', { id: 123, foo: 'bar' }); +// http://yourforum.com/users/123?foo=bar +``` -// Link can be used just like any other component: -Hello World! +The extender also allows you to define a route helper method: -// You'll frequently use Link with generated routes: -Hello World! +```js + new Extend.Routes() + .add('acme.user', '/user/:id', ) + .helper('acmeUser', (user) => app.route('acme.user', { id: user.id() })) +``` -// Link can even generate external links with the external attr: -Hello World! +This allows you to generate URLs to the route using the `acmeUser` helper method: -// The above example with external = true is equivalent to: -Hello World! -// but is provided for flexibility: sometimes you might have links -// that are conditionally internal or external. +```js +const url = app.route.acmeUser(user); +// http://yourforum.com/users/123 ``` ### Enlaces a otras páginas -Un foro no sería muy útil si sólo tuviera una página. Mientras que usted podría, por supuesto, implementar enlaces a otras partes de su foro con etiquetas de anclaje HTML y enlaces codificados, esto puede ser difícil de mantener, y derrota el propósito de que Flarum sea una [Single Page Application](https://en.wikipedia.org/wiki/Single-page_application) en primer lugar. +A forum wouldn't be very useful if it only had one page. While you could, of course, implement links to other parts of your forum with HTML anchor tags and hardcoded links, this can be difficult to maintain, and defeats the purpose of Flarum being a [Single Page Application](https://en.wikipedia.org/wiki/Single-page_application) in the first place. -Flarum utiliza la API de enrutamiento de Mithril para proporcionar un componente `Link` que envuelve limpiamente los enlaces a otras páginas internas. Su uso es bastante simple: +Flarum uses Mithril's routing API to provide a `Link` component that neatly wraps links to other internal pages. Its use is fairly simple: ```jsx import Link from 'flarum/common/components/Link'; @@ -237,14 +248,14 @@ import Link from 'flarum/common/components/Link'; ## Contenido -Cada vez que visitas una ruta del frontend, el backend construye un documento HTML con el andamiaje necesario para arrancar la aplicación JavaScript del frontend. Puedes modificar fácilmente este documento para realizar tareas como: +Whenever you visit a frontend route, the backend constructs a HTML document with the scaffolding necessary to boot up the frontend JavaScript application. You can easily modify this document to perform tasks like: * Cambiar el `` de la página * Añadir recursos externos de JavaScript y CSS * Añadir contenido SEO y etiquetas `<meta>`. * Añadir datos a la carga útil de JavaScript (por ejemplo, para precargar los recursos que se van a renderizar en la página inmediatamente, evitando así una petición innecesaria a la API) -Puedes hacer cambios en el frontend usando el método `content` del extensor `Frontend`. Este método acepta un cierre que recibe dos parámetros: un objeto `Flarum\Frontend\Document` que representa el documento HTML que se mostrará, y el objeto `Request`. +You can make blanket changes to the frontend using the `Frontend` extender's `content` method. This accepts a closure which receives two parameters: a `Flarum\Frontend\Document` object which represents the HTML document that will be displayed, and the `Request` object. ```php use Flarum\Frontend\Document; @@ -258,7 +269,7 @@ return [ ]; ``` -También puede añadir contenido en sus registros de ruta de frontend: +You can also add content onto your frontend route registrations: ```php return [ diff --git a/i18n/es/docusaurus-plugin-content-docs/current/extend/testing.md b/i18n/es/docusaurus-plugin-content-docs/current/extend/testing.md index e84669186..838725732 100644 --- a/i18n/es/docusaurus-plugin-content-docs/current/extend/testing.md +++ b/i18n/es/docusaurus-plugin-content-docs/current/extend/testing.md @@ -18,8 +18,6 @@ $ flarum-cli infra backendTesting ::: -Firstly, you will need to require the `flarum/testing` composer package as a dev dependency for your extension: - ```bash composer require --dev flarum/testing:^1.0 ``` @@ -400,7 +398,167 @@ NOTE: If you find your extension needs _lots and lots_ of mocks, or mocks that f ## Frontend Tests -Coming Soon! +### Configuración + +:::tip [Flarum CLI](https://github.com/flarum/cli) + +You can use the CLI to automatically add and update frontend testing infrastructure to your code: + +```bash +$ flarum-cli infra frontendTesting +``` + +::: + +First, you need to install the Jest config dev dependency: + +```bash +$ yarn add --dev @flarum/jest-config +``` + +Then, add the following to your `package.json`: + +```json +{ + "type": "module", + "scripts": { + ..., + "test": "yarn node --experimental-vm-modules $(yarn bin jest)" + } +} +``` + +Rename `webpack.config.js` to `webpack.config.cjs`. This is necessary because Jest doesn't support ESM yet. + +Create a `jest.config.cjs` file in the root of your extension: + +```js +module.exports = require('@flarum/jest-config')(); +``` + +If you are using TypeScript, create tsconfig.test.json with the following content: + +```json +{ + "extends": "./tsconfig.json", + "include": ["tests/**/*"], + "files": ["../../../node_modules/@flarum/jest-config/shims.d.ts"] +} +``` + +Then, you will need to set up a file structure for tests: + +``` +js +├── dist +├── src +├── tests +│ ├── unit +│ │ └── functionTest.test.js +│ ├── integration +│ │ └── componentTest.test.js +├── package.json +├── tsconfig.json +├── tsconfig.test.json +├── jest.config.cjs +└── webpack.config.cjs +``` + +#### Flujo de trabajo de pruebas en Github + +To run tests on every commit and pull request, check out the [GitHub Actions](github-actions.md) page. + +### Using Unit Tests + +Like any other JS project, you can use Jest to write unit tests for your frontend code. Checkout the [Jest docs](https://jestjs.io/docs/using-matchers) for more information on how to write tests. + +Here's a simple example of a unit test fo core's `abbreviateNumber` function: + +```ts +import abbreviateNumber from '../../../../src/common/utils/abbreviateNumber'; + +test('does not change small numbers', () => { + expect(abbreviateNumber(1)).toBe('1'); +}); + +test('abbreviates large numbers', () => { + expect(abbreviateNumber(1000000)).toBe('1M'); + expect(abbreviateNumber(100500)).toBe('100.5K'); +}); + +test('abbreviates large numbers with decimal places', () => { + expect(abbreviateNumber(100500)).toBe('100.5K'); + expect(abbreviateNumber(13234)).toBe('13.2K'); +}); +``` + +### Uso de las pruebas de integración + +Integration tests are used to test the components of your frontend code and the interaction between different components. For example, you might test that a page component renders the correct content based on certain parameters. + +Here's a simple example of an integration test for core's `Alert` component: + +```ts +import Alert from '../../../../src/common/components/Alert'; +import m from 'mithril'; +import mq from 'mithril-query'; +import { jest } from '@jest/globals'; + +describe('Alert displays as expected', () => { + it('should display alert messages with an icon', () => { + const alert = mq(m(Alert, { type: 'error' }, 'Shoot!')); + expect(alert).toContainRaw('Shoot!'); + expect(alert).toHaveElement('i.icon'); + }); + + it('should display alert messages with a custom icon when using a title', () => { + const alert = mq(Alert, { type: 'error', icon: 'fas fa-users', title: 'Woops..' }); + expect(alert).toContainRaw('Woops..'); + expect(alert).toHaveElement('i.fas.fa-users'); + }); + + it('should display alert messages with a title', () => { + const alert = mq(m(Alert, { type: 'error', title: 'Error Title' }, 'Shoot!')); + expect(alert).toContainRaw('Shoot!'); + expect(alert).toContainRaw('Error Title'); + expect(alert).toHaveElement('.Alert-title'); + }); + + it('should display alert messages with custom controls', () => { + const alert = mq(Alert, { type: 'error', controls: [m('button', { className: 'Button--test' }, 'Click me!')] }); + expect(alert).toHaveElement('button.Button--test'); + }); +}); + +describe('Alert is dismissible', () => { + it('should show dismiss button', function () { + const alert = mq(m(Alert, { dismissible: true }, 'Shoot!')); + expect(alert).toHaveElement('button.Alert-dismiss'); + }); + + it('should call ondismiss when dismiss button is clicked', function () { + const ondismiss = jest.fn(); + const alert = mq(Alert, { dismissible: true, ondismiss }); + alert.click('.Alert-dismiss'); + expect(ondismiss).toHaveBeenCalled(); + }); + + it('should not be dismissible if not chosen', function () { + const alert = mq(Alert, { type: 'error', dismissible: false }); + expect(alert).not.toHaveElement('button.Alert-dismiss'); + }); +}); +``` + +#### Methods + +These are the custom methods that are available for mithril component tests: +* **`toHaveElement(selector)`** - Checks if the component has an element that matches the given selector. +* **`toContainRaw(content)`** - Checks if the component HTML contains the given content. + +To negate any of these methods, simply prefix them with `not.`. For example, `expect(alert).not.toHaveElement('button.Alert-dismiss');`. For more information, check out the [Jest docs](https://jestjs.io/docs/using-matchers). For example you may need to check how to [mock functions](https://jestjs.io/docs/mock-functions), or how to use `beforeEach` and `afterEach` to set up and tear down tests. + + ## E2E Tests diff --git a/i18n/it/docusaurus-plugin-content-docs/current/extend/api.md b/i18n/it/docusaurus-plugin-content-docs/current/extend/api.md index 350468f32..4121180ad 100644 --- a/i18n/it/docusaurus-plugin-content-docs/current/extend/api.md +++ b/i18n/it/docusaurus-plugin-content-docs/current/extend/api.md @@ -331,7 +331,7 @@ return [ (new Extend\ApiSerializer(UserSerializer::class)) // One attribute at a time ->attribute('firstName', function ($serializer, $user, $attributes) { - return $user->first_name + return $user->first_name }) // Multiple modifications at once, more complex logic ->mutate(function($serializer, $user, $attributes) { diff --git a/i18n/it/docusaurus-plugin-content-docs/current/extend/github-actions.md b/i18n/it/docusaurus-plugin-content-docs/current/extend/github-actions.md index 7f1c222b7..c8514c6cc 100644 --- a/i18n/it/docusaurus-plugin-content-docs/current/extend/github-actions.md +++ b/i18n/it/docusaurus-plugin-content-docs/current/extend/github-actions.md @@ -4,8 +4,6 @@ In public repos, [GitHub Actions](https://github.com/features/actions) allow you In this guide, you will learn how to add pre-defined workflows to your extension. -## Setup - :::tip [Flarum CLI](https://github.com/flarum/cli) You can use the CLI to automatically add and update workflows to your code: @@ -15,6 +13,9 @@ $ flarum-cli infra githubActions ::: +## Backend + + All you need to do is create a `.github/workflows/backend.yml` file in your extension, it will reuse a predefined workflow by the core development team which can be found [here](https://github.com/flarum/framework/blob/main/.github/workflows/REUSABLE_backend.yml). You need to specify the configuration as follows: ```yaml @@ -34,9 +35,7 @@ jobs: backend_directory: . ``` -## Backend - -Flarum provides a pre-defined workflow for running certain jobs for the backend of your extension. These are the currently available jobs: +These are the currently available jobs: | Name | Key | Descrizione | | ----------------------------------------------- | ------------------------ | -------------------------------------- | @@ -69,4 +68,52 @@ For more details on parameters, [checkout the full predefined reusable workflow ## Frontend -Soon.. +All you need to do is create a `.github/workflows/frontend.yml` file in your extension, it will reuse a predefined workflow by the core development team which can be found [here](https://github.com/flarum/framework/blob/main/.github/workflows/REUSABLE_frontend.yml). You need to specify the configuration as follows: + +```yaml +name: Frontend + +on: [workflow_dispatch, push, pull_request] + +jobs: + run: + uses: flarum/framework/.github/workflows/REUSABLE_frontend.yml@main + with: + enable_bundlewatch: false + enable_prettier: true + enable_typescript: false + + frontend_directory: ./js + backend_directory: . + js_package_manager: yarn + main_git_branch: main + + secrets: + bundlewatch_github_token: ${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }} +``` + +Unlike the backend workflow, the frontend workflow runs everything in a single job. Here are the available parameters: + +| Name | Key | Descrizione | Format | +| --------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| Build Script | `build_script` | Script to run for production build. Empty value to disable. | string | +| Build Typings Script | `build_typings_script` | Script to run for typings build. Empty value to disable. | string | +| Format Script | `format_script` | Script to run for code formatting. Empty value to disable. | string | +| Check Typings Script | `check_typings_script` | Script to run for tyiping check. Empty value to disable. | string | +| Type Coverage Script | `type_coverage_script` | Script to run for type coverage. Empty value to disable. | string | +| Test Script | `test_script` | Script to run for tests. Empty value to disable. | string | +| Enable Bundlewatch | `enable_bundlewatch` | Enable Bundlewatch? | string | +| Enable Prettier | `enable_prettier` | Enable Prettier? | string | +| Enable Typescript | `enable_typescript` | Enable TypeScript? | string | +| Enable Tests | `enable_tests` | Enable Tests? | string | +| Backend Directory | `backend_directory` | The directory of the project where backend code is located. This should contain a `composer.json` file, and is generally the root directory of the repo. | string | +| Frontend Directory | `frontend_directory` | The directory of the project where frontend code is located. This should contain a `package.json` file. | string | +| Main Git Branch | `main_git_branch` | The main git branch to use for the workflow. | string | +| Node Version | `node_version` | The node version to use for the workflow. | string | +| JS Package Manager | `js_package_manager` | The package manager to use (ex. yarn) | string | +| Cache Dependency Path | `cache_dependency_path` | The path to the cache dependency file. | string | + :::tip + +For more details on parameters, [checkout the full predefined reusable workflow file](https://github.com/flarum/framework/blob/main/.github/workflows/REUSABLE_frontend.yml). + +::: diff --git a/i18n/it/docusaurus-plugin-content-docs/current/extend/models.md b/i18n/it/docusaurus-plugin-content-docs/current/extend/models.md index 9dbc19698..ee01a5899 100644 --- a/i18n/it/docusaurus-plugin-content-docs/current/extend/models.md +++ b/i18n/it/docusaurus-plugin-content-docs/current/extend/models.md @@ -252,39 +252,40 @@ export default class Tag extends Model { } ``` -You must then register your new model with the store: +You must then register your new model with the store using the frontend `Store` extender in a new `extend.js` module: ```js -app.store.models.tags = Tag; +import Extend from 'flarum/common/extenders'; + +export default [ + new Extend.Store() + .add('tags', Tag), +]; ``` +:::info -<!-- You must then register your new model with the store using the `Model` extender: +Remember to export the `extend` module from your entry `index.js` file: ```js -export const extend = [ - new Extend.Model('tags', Tag) -]; -``` --> -### Extending Models -To add attributes and relationships to existing models, modify the model class prototype: - -```js -Discussion.prototype.user = Model.hasOne('user'); -Discussion.prototype.posts = Model.hasMany('posts'); -Discussion.prototype.slug = Model.attribute('slug'); +export { default as extend } from './extend'; ``` +::: -<!-- To add attributes and relationships to existing models, use the `Model` extender: +### Extending Models + +To add attributes and relationships to existing models, use the `Model` extender: + +```ts + new Extend.Model(Discussion) + .attribute<string>('slug') + .hasOne<User>('user') + .hasMany<Post>('posts') +``` -```js - new Extend.Model('discussions') - .attribute('slug') - .hasOne('user') - .hasMany('posts') -``` --> ### Saving Resources + To send data back through the API, call the `save` method on a model instance. This method returns a Promise which resolves with the same model instance: ```js diff --git a/i18n/it/docusaurus-plugin-content-docs/current/extend/routes.md b/i18n/it/docusaurus-plugin-content-docs/current/extend/routes.md index 5003697d2..84a3c1855 100644 --- a/i18n/it/docusaurus-plugin-content-docs/current/extend/routes.md +++ b/i18n/it/docusaurus-plugin-content-docs/current/extend/routes.md @@ -160,91 +160,102 @@ Sul backend, invece di aggiungere il tuo percorso frontend tramite `Routes`, pot Ora quando `tuoforum.com/utente` viene visitato, verrà visualizzato il frontend del forum. Tuttavia, poiché il frontend non conosce ancora la rotta `users`, verrà visualizzato l'elenco di discussioni. -Flarum si basa sul [sistema di instradamento di Mithril](https://mithril.js.org/index.html#routing), che aggiunge nomi di percorsi e una classe astratta per le pagine (`common/components/Page`). Per registrare un nuovo percorso, aggiungi un oggetto `app.routes`: +Flarum builds on [Mithril's routing system](https://mithril.js.org/index.html#routing), adding route names and an abstract class for pages (`common/components/Page`). + +To register the route on the frontend, there is a `Routes` extender which works much like the backend one. Instead of a controller, however, you pass a component instance as the third argument: + +```jsx +import Extend from 'flarum/common/extenders'; + +export default [ + new Extend.Routes() + .add('acme.users', '/users', <UsersPage />), +]; +``` + +:::info + +Remember to export the `extend` module from your entry `index.js` file: ```js -app.routes['acme.users'] = { path: '/users', component: UsersPage }; +export { default as extend } from './extend'; ``` +::: -<!-- To register the route on the frontend, there is a `Routes` extender which works much like the backend one. Instead of a controller, however, you pass a component instance as the third argument: +Now when `yourforum.com/users` is visited, the forum frontend will be loaded and the `UsersPage` component will be rendered in the content area. For more information on frontend pages, please see [that documentation section](frontend-pages.md). + +Advanced use cases might also be interested in using [route resolvers](frontend-pages.md#route-resolvers-advanced). -```jsx -export const extend = [ - new Extend.Routes() - .add('/users', 'acme.users', <UsersPage />) -]; -``` --> -Ora quanto `tuoforum.com/utente` verrà caricato il frontend del forum ed anche il componente `UsersPage` verrà renderizzato. Per ulteriori informazioni sulle pagine di frontend, vedere [questa sezione della documentazione](frontend-pages.md). -I casi di utilizzo avanzato potrebbero anche essere interessati a guardare i [risolutori di percorsi](frontend-pages.md#route-resolvers-advanced). ### Parametri percorsi -I percorsi delfrontend consentono anche di acquisire segmenti dell'URI, ma la [sintassi di Mithril](https://mithril.js.org/route.html) è leggermente diversa: + +Frontend routes also allow you to capture segments of the URI: ```jsx -app.routes['acme.user'] = { path: '/user/:id', component: UserPage }; + new Extend.Routes() + .add('acme.user', '/user/:id', <UsersPage />) ``` +Route parameters will be passed into the `attrs` of the route's component. They will also be available through [`m.route.param`](https://mithril.js.org/route.html#mrouteparam) -<!-- ```jsx - new Extend.Routes() - .add('/user/:id', 'acme.user', <UsersPage />) -``` --> -I parametri del percorso verranno passati in `attrs` del componente. Saranno disponibili anche tramite [`m.route.param`](https://mithril.js.org/route.html#mrouteparam) ### Generare URL -Per generare un URL ad un percorso sul frontend, utilizzare il metodo `app.route`. Accetta due argomenti: il nome della rotta e un hash di parametri. I parametri riempiranno i segmenti URI corrispondenti, altrimenti verranno aggiunti come parametri della query. + +To generate a URL to a route on the frontend, use the `app.route` method. This accepts two arguments: the route name, and a hash of parameters. I parametri riempiranno i segmenti URI corrispondenti, altrimenti verranno aggiunti come parametri della query. ```js -import Link from 'flarum/components/Link'; +const url = app.route('acme.user', { id: 123, foo: 'bar' }); +// http://yourforum.com/users/123?foo=bar +``` -// Link can be used just like any other component: -<Link href="/route/known/to/mithril">Hello World!</Link> +The extender also allows you to define a route helper method: -// You'll frequently use Link with generated routes: -<Link href={app.route('settings')}>Hello World!</Link> +```js + new Extend.Routes() + .add('acme.user', '/user/:id', <UsersPage />) + .helper('acmeUser', (user) => app.route('acme.user', { id: user.id() })) +``` -// Link can even generate external links with the external attr: -<Link external={true} href="https://google.com">Hello World!</Link> +This allows you to generate URLs to the route using the `acmeUser` helper method: -// The above example with external = true is equivalent to: -<a href="https://google.com">Hello World!</a> -// but is provided for flexibility: sometimes you might have links -// that are conditionally internal or external. +```js +const url = app.route.acmeUser(user); +// http://yourforum.com/users/123 ``` ### Collegamenti ad altre pagine -Un forum non sarebbe molto utile se avesse solo una pagina. Sebbene tu possa, ovviamente, implementare link ad altre parti del tuo forum con tag di ancoraggio HTML e link , questi possono essere difficile da mantenere e vanificano lo scopo di Flarum di essere una [Applicazione a pagina singola](https://en.wikipedia.org/wiki/Single-page_application). +A forum wouldn't be very useful if it only had one page. While you could, of course, implement links to other parts of your forum with HTML anchor tags and hardcoded links, this can be difficult to maintain, and defeats the purpose of Flarum being a [Single Page Application](https://en.wikipedia.org/wiki/Single-page_application) in the first place. -Flarum utilizza l'API di routing di Mithril per fornire un componente `Link` che racchiude in modo ordinato i collegamenti ad altre pagine interne. Il suo utilizzo è abbastanza semplice: +Flarum uses Mithril's routing API to provide a `Link` component that neatly wraps links to other internal pages. Its use is fairly simple: ```jsx import Link from 'flarum/common/components/Link'; -// Link può essere usato come qualsiasi altro componente: -<Link href="/route/known/to/mithril">Ciao Mondo!</Link> +// Link can be used just like any other component: +<Link href="/route/known/to/mithril">Hello World!</Link> -// Utilizzerai frequentemente Link con itinerari generati: -<Link href={app.route('settings')}>Ciao Mondo!</Link> +// You'll frequently use Link with generated routes: +<Link href={app.route('settings')}>Hello World!</Link> -// Il link può anche generare collegamenti esterni con attributi esterni: -<Link external={true} href="https://google.com">Ciao Mondo!</Link> +// Link can even generate external links with the external attr: +<Link external={true} href="https://google.com">Hello World!</Link> -// L'esempio precedente con esterno = true equivale a: -<a href="https://google.com">Ciao Mondo!</a> -// ma è previsto per flessibilità: a volte si possono avere link -// che sono condizionalmente interni o esterni. +// The above example with external = true is equivalent to: +<a href="https://google.com">Hello World!</a> +// but is provided for flexibility: sometimes you might have links +// that are conditionally internal or external. ``` ## Contenuto -Ogni volta che visiti percorso sul frontend, il backend costruisce un documento HTML con lo "scheletro" necessario per avviare l'applicazione JavaScript frontend. Puoi facilmente modificare questo documento per eseguire attività come: +Whenever you visit a frontend route, the backend constructs a HTML document with the scaffolding necessary to boot up the frontend JavaScript application. You can easily modify this document to perform tasks like: * Modificare il `<title>` della pagina * Aggiunta di risorse JavaScript e CSS esterne * Aggiunta di contenuti SEO e tag `<meta>` * Aggiunta di dati al payload JavaScript (ad es. Per precaricare le risorse che verranno visualizzate immediatamente sulla pagina, evitando così una richiesta non necessaria all'API) -Puoi apportare modifiche generali al frontend utilizzando l'extender `Frontend` e metodo `content`. Accetta una chiusura che riceve due parametri: un oggetto `Flarum\Frontend\Document` che rappresenta il documento HTML che verrà visualizzato e un oggetto `Request`. +You can make blanket changes to the frontend using the `Frontend` extender's `content` method. This accepts a closure which receives two parameters: a `Flarum\Frontend\Document` object which represents the HTML document that will be displayed, and the `Request` object. ```php use Flarum\Frontend\Document; @@ -258,7 +269,7 @@ return [ ]; ``` -Puoi anche aggiungere contenuti tuo frontend: +You can also add content onto your frontend route registrations: ```php return [ diff --git a/i18n/it/docusaurus-plugin-content-docs/current/extend/testing.md b/i18n/it/docusaurus-plugin-content-docs/current/extend/testing.md index f516478f0..7b45a1f70 100644 --- a/i18n/it/docusaurus-plugin-content-docs/current/extend/testing.md +++ b/i18n/it/docusaurus-plugin-content-docs/current/extend/testing.md @@ -18,8 +18,6 @@ $ flarum-cli infra backendTesting ::: -Firstly, you will need to require the `flarum/testing` composer package as a dev dependency for your extension: - ```bash composer require --dev flarum/testing:^1.0 ``` @@ -400,7 +398,167 @@ NOTE: If you find your extension needs _lots and lots_ of mocks, or mocks that f ## Frontend Tests -Coming Soon! +### Setup + +:::tip [Flarum CLI](https://github.com/flarum/cli) + +You can use the CLI to automatically add and update frontend testing infrastructure to your code: + +```bash +$ flarum-cli infra frontendTesting +``` + +::: + +First, you need to install the Jest config dev dependency: + +```bash +$ yarn add --dev @flarum/jest-config +``` + +Then, add the following to your `package.json`: + +```json +{ + "type": "module", + "scripts": { + ..., + "test": "yarn node --experimental-vm-modules $(yarn bin jest)" + } +} +``` + +Rename `webpack.config.js` to `webpack.config.cjs`. This is necessary because Jest doesn't support ESM yet. + +Create a `jest.config.cjs` file in the root of your extension: + +```js +module.exports = require('@flarum/jest-config')(); +``` + +If you are using TypeScript, create tsconfig.test.json with the following content: + +```json +{ + "extends": "./tsconfig.json", + "include": ["tests/**/*"], + "files": ["../../../node_modules/@flarum/jest-config/shims.d.ts"] +} +``` + +Then, you will need to set up a file structure for tests: + +``` +js +├── dist +├── src +├── tests +│ ├── unit +│ │ └── functionTest.test.js +│ ├── integration +│ │ └── componentTest.test.js +├── package.json +├── tsconfig.json +├── tsconfig.test.json +├── jest.config.cjs +└── webpack.config.cjs +``` + +#### GitHub Testing Workflow + +To run tests on every commit and pull request, check out the [GitHub Actions](github-actions.md) page. + +### Using Unit Tests + +Like any other JS project, you can use Jest to write unit tests for your frontend code. Checkout the [Jest docs](https://jestjs.io/docs/using-matchers) for more information on how to write tests. + +Here's a simple example of a unit test fo core's `abbreviateNumber` function: + +```ts +import abbreviateNumber from '../../../../src/common/utils/abbreviateNumber'; + +test('does not change small numbers', () => { + expect(abbreviateNumber(1)).toBe('1'); +}); + +test('abbreviates large numbers', () => { + expect(abbreviateNumber(1000000)).toBe('1M'); + expect(abbreviateNumber(100500)).toBe('100.5K'); +}); + +test('abbreviates large numbers with decimal places', () => { + expect(abbreviateNumber(100500)).toBe('100.5K'); + expect(abbreviateNumber(13234)).toBe('13.2K'); +}); +``` + +### Using Integration Tests + +Integration tests are used to test the components of your frontend code and the interaction between different components. For example, you might test that a page component renders the correct content based on certain parameters. + +Here's a simple example of an integration test for core's `Alert` component: + +```ts +import Alert from '../../../../src/common/components/Alert'; +import m from 'mithril'; +import mq from 'mithril-query'; +import { jest } from '@jest/globals'; + +describe('Alert displays as expected', () => { + it('should display alert messages with an icon', () => { + const alert = mq(m(Alert, { type: 'error' }, 'Shoot!')); + expect(alert).toContainRaw('Shoot!'); + expect(alert).toHaveElement('i.icon'); + }); + + it('should display alert messages with a custom icon when using a title', () => { + const alert = mq(Alert, { type: 'error', icon: 'fas fa-users', title: 'Woops..' }); + expect(alert).toContainRaw('Woops..'); + expect(alert).toHaveElement('i.fas.fa-users'); + }); + + it('should display alert messages with a title', () => { + const alert = mq(m(Alert, { type: 'error', title: 'Error Title' }, 'Shoot!')); + expect(alert).toContainRaw('Shoot!'); + expect(alert).toContainRaw('Error Title'); + expect(alert).toHaveElement('.Alert-title'); + }); + + it('should display alert messages with custom controls', () => { + const alert = mq(Alert, { type: 'error', controls: [m('button', { className: 'Button--test' }, 'Click me!')] }); + expect(alert).toHaveElement('button.Button--test'); + }); +}); + +describe('Alert is dismissible', () => { + it('should show dismiss button', function () { + const alert = mq(m(Alert, { dismissible: true }, 'Shoot!')); + expect(alert).toHaveElement('button.Alert-dismiss'); + }); + + it('should call ondismiss when dismiss button is clicked', function () { + const ondismiss = jest.fn(); + const alert = mq(Alert, { dismissible: true, ondismiss }); + alert.click('.Alert-dismiss'); + expect(ondismiss).toHaveBeenCalled(); + }); + + it('should not be dismissible if not chosen', function () { + const alert = mq(Alert, { type: 'error', dismissible: false }); + expect(alert).not.toHaveElement('button.Alert-dismiss'); + }); +}); +``` + +#### Methods + +These are the custom methods that are available for mithril component tests: +* **`toHaveElement(selector)`** - Checks if the component has an element that matches the given selector. +* **`toContainRaw(content)`** - Checks if the component HTML contains the given content. + +To negate any of these methods, simply prefix them with `not.`. For example, `expect(alert).not.toHaveElement('button.Alert-dismiss');`. For more information, check out the [Jest docs](https://jestjs.io/docs/using-matchers). For example you may need to check how to [mock functions](https://jestjs.io/docs/mock-functions), or how to use `beforeEach` and `afterEach` to set up and tear down tests. + + ## E2E Tests diff --git a/i18n/tr/docusaurus-plugin-content-docs/current/extend/api.md b/i18n/tr/docusaurus-plugin-content-docs/current/extend/api.md index f5ca9b1f5..ee9a01f50 100644 --- a/i18n/tr/docusaurus-plugin-content-docs/current/extend/api.md +++ b/i18n/tr/docusaurus-plugin-content-docs/current/extend/api.md @@ -331,7 +331,7 @@ return [ (new Extend\ApiSerializer(UserSerializer::class)) // One attribute at a time ->attribute('firstName', function ($serializer, $user, $attributes) { - return $user->first_name + return $user->first_name }) // Multiple modifications at once, more complex logic ->mutate(function($serializer, $user, $attributes) { diff --git a/i18n/tr/docusaurus-plugin-content-docs/current/extend/github-actions.md b/i18n/tr/docusaurus-plugin-content-docs/current/extend/github-actions.md index d810720cd..904bdf995 100644 --- a/i18n/tr/docusaurus-plugin-content-docs/current/extend/github-actions.md +++ b/i18n/tr/docusaurus-plugin-content-docs/current/extend/github-actions.md @@ -4,8 +4,6 @@ In public repos, [GitHub Actions](https://github.com/features/actions) allow you In this guide, you will learn how to add pre-defined workflows to your extension. -## Setup - :::tip [Flarum CLI](https://github.com/flarum/cli) You can use the CLI to automatically add and update workflows to your code: @@ -15,6 +13,9 @@ $ flarum-cli infra githubActions ::: +## Backend + + All you need to do is create a `.github/workflows/backend.yml` file in your extension, it will reuse a predefined workflow by the core development team which can be found [here](https://github.com/flarum/framework/blob/main/.github/workflows/REUSABLE_backend.yml). You need to specify the configuration as follows: ```yaml @@ -34,9 +35,7 @@ jobs: backend_directory: . ``` -## Backend - -Flarum provides a pre-defined workflow for running certain jobs for the backend of your extension. These are the currently available jobs: +These are the currently available jobs: | Name | Key | Description | | ----------------------------------------------- | ------------------------ | -------------------------------------- | @@ -69,4 +68,52 @@ For more details on parameters, [checkout the full predefined reusable workflow ## Frontend -Soon.. +All you need to do is create a `.github/workflows/frontend.yml` file in your extension, it will reuse a predefined workflow by the core development team which can be found [here](https://github.com/flarum/framework/blob/main/.github/workflows/REUSABLE_frontend.yml). You need to specify the configuration as follows: + +```yaml +name: Frontend + +on: [workflow_dispatch, push, pull_request] + +jobs: + run: + uses: flarum/framework/.github/workflows/REUSABLE_frontend.yml@main + with: + enable_bundlewatch: false + enable_prettier: true + enable_typescript: false + + frontend_directory: ./js + backend_directory: . + js_package_manager: yarn + main_git_branch: main + + secrets: + bundlewatch_github_token: ${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }} +``` + +Unlike the backend workflow, the frontend workflow runs everything in a single job. Here are the available parameters: + +| Name | Key | Description | Format | +| --------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| Build Script | `build_script` | Script to run for production build. Empty value to disable. | string | +| Build Typings Script | `build_typings_script` | Script to run for typings build. Empty value to disable. | string | +| Format Script | `format_script` | Script to run for code formatting. Empty value to disable. | string | +| Check Typings Script | `check_typings_script` | Script to run for tyiping check. Empty value to disable. | string | +| Type Coverage Script | `type_coverage_script` | Script to run for type coverage. Empty value to disable. | string | +| Test Script | `test_script` | Script to run for tests. Empty value to disable. | string | +| Enable Bundlewatch | `enable_bundlewatch` | Enable Bundlewatch? | string | +| Enable Prettier | `enable_prettier` | Enable Prettier? | string | +| Enable Typescript | `enable_typescript` | Enable TypeScript? | string | +| Enable Tests | `enable_tests` | Enable Tests? | string | +| Backend Directory | `backend_directory` | The directory of the project where backend code is located. This should contain a `composer.json` file, and is generally the root directory of the repo. | string | +| Frontend Directory | `frontend_directory` | The directory of the project where frontend code is located. This should contain a `package.json` file. | string | +| Main Git Branch | `main_git_branch` | The main git branch to use for the workflow. | string | +| Node Version | `node_version` | The node version to use for the workflow. | string | +| JS Package Manager | `js_package_manager` | The package manager to use (ex. yarn) | string | +| Cache Dependency Path | `cache_dependency_path` | The path to the cache dependency file. | string | + :::tip + +For more details on parameters, [checkout the full predefined reusable workflow file](https://github.com/flarum/framework/blob/main/.github/workflows/REUSABLE_frontend.yml). + +::: diff --git a/i18n/tr/docusaurus-plugin-content-docs/current/extend/models.md b/i18n/tr/docusaurus-plugin-content-docs/current/extend/models.md index 822f354f3..5fecd6926 100644 --- a/i18n/tr/docusaurus-plugin-content-docs/current/extend/models.md +++ b/i18n/tr/docusaurus-plugin-content-docs/current/extend/models.md @@ -252,39 +252,40 @@ export default class Tag extends Model { } ``` -You must then register your new model with the store: +You must then register your new model with the store using the frontend `Store` extender in a new `extend.js` module: ```js -app.store.models.tags = Tag; +import Extend from 'flarum/common/extenders'; + +export default [ + new Extend.Store() + .add('tags', Tag), +]; ``` +:::info -<!-- You must then register your new model with the store using the `Model` extender: +Remember to export the `extend` module from your entry `index.js` file: ```js -export const extend = [ - new Extend.Model('tags', Tag) -]; -``` --> -### Extending Models -To add attributes and relationships to existing models, modify the model class prototype: - -```js -Discussion.prototype.user = Model.hasOne('user'); -Discussion.prototype.posts = Model.hasMany('posts'); -Discussion.prototype.slug = Model.attribute('slug'); +export { default as extend } from './extend'; ``` +::: -<!-- To add attributes and relationships to existing models, use the `Model` extender: +### Extending Models + +To add attributes and relationships to existing models, use the `Model` extender: + +```ts + new Extend.Model(Discussion) + .attribute<string>('slug') + .hasOne<User>('user') + .hasMany<Post>('posts') +``` -```js - new Extend.Model('discussions') - .attribute('slug') - .hasOne('user') - .hasMany('posts') -``` --> ### Saving Resources + To send data back through the API, call the `save` method on a model instance. This method returns a Promise which resolves with the same model instance: ```js diff --git a/i18n/tr/docusaurus-plugin-content-docs/current/extend/routes.md b/i18n/tr/docusaurus-plugin-content-docs/current/extend/routes.md index 4bbc19dde..3a67b0aaf 100644 --- a/i18n/tr/docusaurus-plugin-content-docs/current/extend/routes.md +++ b/i18n/tr/docusaurus-plugin-content-docs/current/extend/routes.md @@ -160,37 +160,46 @@ On the backend, instead of adding your frontend route via the `Routes` extender, Now when `yourforum.com/users` is visited, the forum frontend will be displayed. However, since the frontend doesn't yet know about the `users` route, the discussion list will still be rendered. -Flarum builds on [Mithril's routing system](https://mithril.js.org/index.html#routing), adding route names and an abstract class for pages (`common/components/Page`). To register a new route, add an object for it to `app.routes`: +Flarum builds on [Mithril's routing system](https://mithril.js.org/index.html#routing), adding route names and an abstract class for pages (`common/components/Page`). -```js -app.routes['acme.users'] = { path: '/users', component: UsersPage }; -``` - - -<!-- To register the route on the frontend, there is a `Routes` extender which works much like the backend one. Instead of a controller, however, you pass a component instance as the third argument: +To register the route on the frontend, there is a `Routes` extender which works much like the backend one. Instead of a controller, however, you pass a component instance as the third argument: ```jsx -export const extend = [ +import Extend from 'flarum/common/extenders'; + +export default [ new Extend.Routes() - .add('/users', 'acme.users', <UsersPage />) + .add('acme.users', '/users', <UsersPage />), ]; -``` --> +``` + +:::info + +Remember to export the `extend` module from your entry `index.js` file: + +```js +export { default as extend } from './extend'; +``` + +::: + Now when `yourforum.com/users` is visited, the forum frontend will be loaded and the `UsersPage` component will be rendered in the content area. For more information on frontend pages, please see [that documentation section](frontend-pages.md). + Advanced use cases might also be interested in using [route resolvers](frontend-pages.md#route-resolvers-advanced). + ### Route Parameters -Frontend routes also allow you to capture segments of the URI, but the [Mithril route syntax](https://mithril.js.org/route.html) is slightly different: + +Frontend routes also allow you to capture segments of the URI: ```jsx -app.routes['acme.user'] = { path: '/user/:id', component: UserPage }; + new Extend.Routes() + .add('acme.user', '/user/:id', <UsersPage />) ``` - -<!-- ```jsx - new Extend.Routes() - .add('/user/:id', 'acme.user', <UsersPage />) -``` --> Route parameters will be passed into the `attrs` of the route's component. They will also be available through [`m.route.param`](https://mithril.js.org/route.html#mrouteparam) + ### Generating URLs + To generate a URL to a route on the frontend, use the `app.route` method. This accepts two arguments: the route name, and a hash of parameters. Parameters will fill in matching URI segments, otherwise they will be appended as query params. ```js @@ -198,6 +207,21 @@ const url = app.route('acme.user', { id: 123, foo: 'bar' }); // http://yourforum.com/users/123?foo=bar ``` +The extender also allows you to define a route helper method: + +```js + new Extend.Routes() + .add('acme.user', '/user/:id', <UsersPage />) + .helper('acmeUser', (user) => app.route('acme.user', { id: user.id() })) +``` + +This allows you to generate URLs to the route using the `acmeUser` helper method: + +```js +const url = app.route.acmeUser(user); +// http://yourforum.com/users/123 +``` + ### Linking to Other Pages A forum wouldn't be very useful if it only had one page. While you could, of course, implement links to other parts of your forum with HTML anchor tags and hardcoded links, this can be difficult to maintain, and defeats the purpose of Flarum being a [Single Page Application](https://en.wikipedia.org/wiki/Single-page_application) in the first place. diff --git a/i18n/tr/docusaurus-plugin-content-docs/current/extend/testing.md b/i18n/tr/docusaurus-plugin-content-docs/current/extend/testing.md index 7b09839ce..56124ff0e 100644 --- a/i18n/tr/docusaurus-plugin-content-docs/current/extend/testing.md +++ b/i18n/tr/docusaurus-plugin-content-docs/current/extend/testing.md @@ -18,8 +18,6 @@ $ flarum-cli infra backendTesting ::: -Firstly, you will need to require the `flarum/testing` composer package as a dev dependency for your extension: - ```bash composer require --dev flarum/testing:^1.0 ``` @@ -400,7 +398,167 @@ NOTE: If you find your extension needs _lots and lots_ of mocks, or mocks that f ## Frontend Tests -Coming Soon! +### Setup + +:::tip [Flarum CLI](https://github.com/flarum/cli) + +You can use the CLI to automatically add and update frontend testing infrastructure to your code: + +```bash +$ flarum-cli infra frontendTesting +``` + +::: + +First, you need to install the Jest config dev dependency: + +```bash +$ yarn add --dev @flarum/jest-config +``` + +Then, add the following to your `package.json`: + +```json +{ + "type": "module", + "scripts": { + ..., + "test": "yarn node --experimental-vm-modules $(yarn bin jest)" + } +} +``` + +Rename `webpack.config.js` to `webpack.config.cjs`. This is necessary because Jest doesn't support ESM yet. + +Create a `jest.config.cjs` file in the root of your extension: + +```js +module.exports = require('@flarum/jest-config')(); +``` + +If you are using TypeScript, create tsconfig.test.json with the following content: + +```json +{ + "extends": "./tsconfig.json", + "include": ["tests/**/*"], + "files": ["../../../node_modules/@flarum/jest-config/shims.d.ts"] +} +``` + +Then, you will need to set up a file structure for tests: + +``` +js +├── dist +├── src +├── tests +│ ├── unit +│ │ └── functionTest.test.js +│ ├── integration +│ │ └── componentTest.test.js +├── package.json +├── tsconfig.json +├── tsconfig.test.json +├── jest.config.cjs +└── webpack.config.cjs +``` + +#### GitHub Testing Workflow + +To run tests on every commit and pull request, check out the [GitHub Actions](github-actions.md) page. + +### Using Unit Tests + +Like any other JS project, you can use Jest to write unit tests for your frontend code. Checkout the [Jest docs](https://jestjs.io/docs/using-matchers) for more information on how to write tests. + +Here's a simple example of a unit test fo core's `abbreviateNumber` function: + +```ts +import abbreviateNumber from '../../../../src/common/utils/abbreviateNumber'; + +test('does not change small numbers', () => { + expect(abbreviateNumber(1)).toBe('1'); +}); + +test('abbreviates large numbers', () => { + expect(abbreviateNumber(1000000)).toBe('1M'); + expect(abbreviateNumber(100500)).toBe('100.5K'); +}); + +test('abbreviates large numbers with decimal places', () => { + expect(abbreviateNumber(100500)).toBe('100.5K'); + expect(abbreviateNumber(13234)).toBe('13.2K'); +}); +``` + +### Using Integration Tests + +Integration tests are used to test the components of your frontend code and the interaction between different components. For example, you might test that a page component renders the correct content based on certain parameters. + +Here's a simple example of an integration test for core's `Alert` component: + +```ts +import Alert from '../../../../src/common/components/Alert'; +import m from 'mithril'; +import mq from 'mithril-query'; +import { jest } from '@jest/globals'; + +describe('Alert displays as expected', () => { + it('should display alert messages with an icon', () => { + const alert = mq(m(Alert, { type: 'error' }, 'Shoot!')); + expect(alert).toContainRaw('Shoot!'); + expect(alert).toHaveElement('i.icon'); + }); + + it('should display alert messages with a custom icon when using a title', () => { + const alert = mq(Alert, { type: 'error', icon: 'fas fa-users', title: 'Woops..' }); + expect(alert).toContainRaw('Woops..'); + expect(alert).toHaveElement('i.fas.fa-users'); + }); + + it('should display alert messages with a title', () => { + const alert = mq(m(Alert, { type: 'error', title: 'Error Title' }, 'Shoot!')); + expect(alert).toContainRaw('Shoot!'); + expect(alert).toContainRaw('Error Title'); + expect(alert).toHaveElement('.Alert-title'); + }); + + it('should display alert messages with custom controls', () => { + const alert = mq(Alert, { type: 'error', controls: [m('button', { className: 'Button--test' }, 'Click me!')] }); + expect(alert).toHaveElement('button.Button--test'); + }); +}); + +describe('Alert is dismissible', () => { + it('should show dismiss button', function () { + const alert = mq(m(Alert, { dismissible: true }, 'Shoot!')); + expect(alert).toHaveElement('button.Alert-dismiss'); + }); + + it('should call ondismiss when dismiss button is clicked', function () { + const ondismiss = jest.fn(); + const alert = mq(Alert, { dismissible: true, ondismiss }); + alert.click('.Alert-dismiss'); + expect(ondismiss).toHaveBeenCalled(); + }); + + it('should not be dismissible if not chosen', function () { + const alert = mq(Alert, { type: 'error', dismissible: false }); + expect(alert).not.toHaveElement('button.Alert-dismiss'); + }); +}); +``` + +#### Methods + +These are the custom methods that are available for mithril component tests: +* **`toHaveElement(selector)`** - Checks if the component has an element that matches the given selector. +* **`toContainRaw(content)`** - Checks if the component HTML contains the given content. + +To negate any of these methods, simply prefix them with `not.`. For example, `expect(alert).not.toHaveElement('button.Alert-dismiss');`. For more information, check out the [Jest docs](https://jestjs.io/docs/using-matchers). For example you may need to check how to [mock functions](https://jestjs.io/docs/mock-functions), or how to use `beforeEach` and `afterEach` to set up and tear down tests. + + ## E2E Tests diff --git a/i18n/vi/docusaurus-plugin-content-docs/current/extend/api.md b/i18n/vi/docusaurus-plugin-content-docs/current/extend/api.md index 542a31400..c2810f98a 100644 --- a/i18n/vi/docusaurus-plugin-content-docs/current/extend/api.md +++ b/i18n/vi/docusaurus-plugin-content-docs/current/extend/api.md @@ -331,7 +331,7 @@ return [ (new Extend\ApiSerializer(UserSerializer::class)) // One attribute at a time ->attribute('firstName', function ($serializer, $user, $attributes) { - return $user->first_name + return $user->first_name }) // Multiple modifications at once, more complex logic ->mutate(function($serializer, $user, $attributes) { diff --git a/i18n/vi/docusaurus-plugin-content-docs/current/extend/github-actions.md b/i18n/vi/docusaurus-plugin-content-docs/current/extend/github-actions.md index 89a9422a7..07a44462e 100644 --- a/i18n/vi/docusaurus-plugin-content-docs/current/extend/github-actions.md +++ b/i18n/vi/docusaurus-plugin-content-docs/current/extend/github-actions.md @@ -4,8 +4,6 @@ In public repos, [GitHub Actions](https://github.com/features/actions) allow you In this guide, you will learn how to add pre-defined workflows to your extension. -## Setup - :::tip [Flarum CLI](https://github.com/flarum/cli) You can use the CLI to automatically add and update workflows to your code: @@ -15,6 +13,9 @@ $ flarum-cli infra githubActions ::: +## Backend + + All you need to do is create a `.github/workflows/backend.yml` file in your extension, it will reuse a predefined workflow by the core development team which can be found [here](https://github.com/flarum/framework/blob/main/.github/workflows/REUSABLE_backend.yml). You need to specify the configuration as follows: ```yaml @@ -34,9 +35,7 @@ jobs: backend_directory: . ``` -## Backend - -Flarum provides a pre-defined workflow for running certain jobs for the backend of your extension. These are the currently available jobs: +These are the currently available jobs: | Name | Key | Mô tả | | ----------------------------------------------- | ------------------------ | -------------------------------------- | @@ -69,4 +68,52 @@ For more details on parameters, [checkout the full predefined reusable workflow ## Frontend -Soon.. +All you need to do is create a `.github/workflows/frontend.yml` file in your extension, it will reuse a predefined workflow by the core development team which can be found [here](https://github.com/flarum/framework/blob/main/.github/workflows/REUSABLE_frontend.yml). You need to specify the configuration as follows: + +```yaml +name: Frontend + +on: [workflow_dispatch, push, pull_request] + +jobs: + run: + uses: flarum/framework/.github/workflows/REUSABLE_frontend.yml@main + with: + enable_bundlewatch: false + enable_prettier: true + enable_typescript: false + + frontend_directory: ./js + backend_directory: . + js_package_manager: yarn + main_git_branch: main + + secrets: + bundlewatch_github_token: ${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }} +``` + +Unlike the backend workflow, the frontend workflow runs everything in a single job. Here are the available parameters: + +| Name | Key | Mô tả | Format | +| --------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| Build Script | `build_script` | Script to run for production build. Empty value to disable. | string | +| Build Typings Script | `build_typings_script` | Script to run for typings build. Empty value to disable. | string | +| Format Script | `format_script` | Script to run for code formatting. Empty value to disable. | string | +| Check Typings Script | `check_typings_script` | Script to run for tyiping check. Empty value to disable. | string | +| Type Coverage Script | `type_coverage_script` | Script to run for type coverage. Empty value to disable. | string | +| Test Script | `test_script` | Script to run for tests. Empty value to disable. | string | +| Enable Bundlewatch | `enable_bundlewatch` | Enable Bundlewatch? | string | +| Enable Prettier | `enable_prettier` | Enable Prettier? | string | +| Enable Typescript | `enable_typescript` | Enable TypeScript? | string | +| Enable Tests | `enable_tests` | Enable Tests? | string | +| Backend Directory | `backend_directory` | The directory of the project where backend code is located. This should contain a `composer.json` file, and is generally the root directory of the repo. | string | +| Frontend Directory | `frontend_directory` | The directory of the project where frontend code is located. This should contain a `package.json` file. | string | +| Main Git Branch | `main_git_branch` | The main git branch to use for the workflow. | string | +| Node Version | `node_version` | The node version to use for the workflow. | string | +| JS Package Manager | `js_package_manager` | The package manager to use (ex. yarn) | string | +| Cache Dependency Path | `cache_dependency_path` | The path to the cache dependency file. | string | + :::tip + +For more details on parameters, [checkout the full predefined reusable workflow file](https://github.com/flarum/framework/blob/main/.github/workflows/REUSABLE_frontend.yml). + +::: diff --git a/i18n/vi/docusaurus-plugin-content-docs/current/extend/models.md b/i18n/vi/docusaurus-plugin-content-docs/current/extend/models.md index 2c2b0a5a1..52e6c7a41 100644 --- a/i18n/vi/docusaurus-plugin-content-docs/current/extend/models.md +++ b/i18n/vi/docusaurus-plugin-content-docs/current/extend/models.md @@ -252,39 +252,40 @@ export default class Tag extends Model { } ``` -You must then register your new model with the store: +You must then register your new model with the store using the frontend `Store` extender in a new `extend.js` module: ```js -app.store.models.tags = Tag; +import Extend from 'flarum/common/extenders'; + +export default [ + new Extend.Store() + .add('tags', Tag), +]; ``` +:::info -<!-- You must then register your new model with the store using the `Model` extender: +Remember to export the `extend` module from your entry `index.js` file: ```js -export const extend = [ - new Extend.Model('tags', Tag) -]; -``` --> -### Extending Models -To add attributes and relationships to existing models, modify the model class prototype: - -```js -Discussion.prototype.user = Model.hasOne('user'); -Discussion.prototype.posts = Model.hasMany('posts'); -Discussion.prototype.slug = Model.attribute('slug'); +export { default as extend } from './extend'; ``` +::: -<!-- To add attributes and relationships to existing models, use the `Model` extender: +### Extending Models + +To add attributes and relationships to existing models, use the `Model` extender: + +```ts + new Extend.Model(Discussion) + .attribute<string>('slug') + .hasOne<User>('user') + .hasMany<Post>('posts') +``` -```js - new Extend.Model('discussions') - .attribute('slug') - .hasOne('user') - .hasMany('posts') -``` --> ### Saving Resources + To send data back through the API, call the `save` method on a model instance. This method returns a Promise which resolves with the same model instance: ```js diff --git a/i18n/vi/docusaurus-plugin-content-docs/current/extend/routes.md b/i18n/vi/docusaurus-plugin-content-docs/current/extend/routes.md index 0fa870656..7ae636129 100644 --- a/i18n/vi/docusaurus-plugin-content-docs/current/extend/routes.md +++ b/i18n/vi/docusaurus-plugin-content-docs/current/extend/routes.md @@ -160,37 +160,46 @@ On the backend, instead of adding your frontend route via the `Routes` extender, Now when `yourforum.com/users` is visited, the forum frontend will be displayed. However, since the frontend doesn't yet know about the `users` route, the discussion list will still be rendered. -Flarum builds on [Mithril's routing system](https://mithril.js.org/index.html#routing), adding route names and an abstract class for pages (`common/components/Page`). To register a new route, add an object for it to `app.routes`: +Flarum builds on [Mithril's routing system](https://mithril.js.org/index.html#routing), adding route names and an abstract class for pages (`common/components/Page`). -```js -app.routes['acme.users'] = { path: '/users', component: UsersPage }; -``` - - -<!-- To register the route on the frontend, there is a `Routes` extender which works much like the backend one. Instead of a controller, however, you pass a component instance as the third argument: +To register the route on the frontend, there is a `Routes` extender which works much like the backend one. Instead of a controller, however, you pass a component instance as the third argument: ```jsx -export const extend = [ +import Extend from 'flarum/common/extenders'; + +export default [ new Extend.Routes() - .add('/users', 'acme.users', <UsersPage />) + .add('acme.users', '/users', <UsersPage />), ]; -``` --> +``` + +:::info + +Remember to export the `extend` module from your entry `index.js` file: + +```js +export { default as extend } from './extend'; +``` + +::: + Now when `yourforum.com/users` is visited, the forum frontend will be loaded and the `UsersPage` component will be rendered in the content area. For more information on frontend pages, please see [that documentation section](frontend-pages.md). + Advanced use cases might also be interested in using [route resolvers](frontend-pages.md#route-resolvers-advanced). + ### Các tham số Route -Frontend routes also allow you to capture segments of the URI, but the [Mithril route syntax](https://mithril.js.org/route.html) is slightly different: + +Frontend routes also allow you to capture segments of the URI: ```jsx -app.routes['acme.user'] = { path: '/user/:id', component: UserPage }; + new Extend.Routes() + .add('acme.user', '/user/:id', <UsersPage />) ``` - -<!-- ```jsx - new Extend.Routes() - .add('/user/:id', 'acme.user', <UsersPage />) -``` --> Route parameters will be passed into the `attrs` of the route's component. They will also be available through [`m.route.param`](https://mithril.js.org/route.html#mrouteparam) + ### Tạo URLs + To generate a URL to a route on the frontend, use the `app.route` method. This accepts two arguments: the route name, and a hash of parameters. Parameters will fill in matching URI segments, otherwise they will be appended as query params. ```js @@ -198,6 +207,21 @@ const url = app.route('acme.user', { id: 123, foo: 'bar' }); // http://yourforum.com/users/123?foo=bar ``` +The extender also allows you to define a route helper method: + +```js + new Extend.Routes() + .add('acme.user', '/user/:id', <UsersPage />) + .helper('acmeUser', (user) => app.route('acme.user', { id: user.id() })) +``` + +This allows you to generate URLs to the route using the `acmeUser` helper method: + +```js +const url = app.route.acmeUser(user); +// http://yourforum.com/users/123 +``` + ### Liên kết đến trang khác A forum wouldn't be very useful if it only had one page. While you could, of course, implement links to other parts of your forum with HTML anchor tags and hardcoded links, this can be difficult to maintain, and defeats the purpose of Flarum being a [Single Page Application](https://en.wikipedia.org/wiki/Single-page_application) in the first place. diff --git a/i18n/vi/docusaurus-plugin-content-docs/current/extend/testing.md b/i18n/vi/docusaurus-plugin-content-docs/current/extend/testing.md index bf033dacf..f9d866094 100644 --- a/i18n/vi/docusaurus-plugin-content-docs/current/extend/testing.md +++ b/i18n/vi/docusaurus-plugin-content-docs/current/extend/testing.md @@ -18,8 +18,6 @@ $ flarum-cli infra backendTesting ::: -Firstly, you will need to require the `flarum/testing` composer package as a dev dependency for your extension: - ```bash composer require --dev flarum/testing:^1.0 ``` @@ -400,7 +398,167 @@ NOTE: If you find your extension needs _lots and lots_ of mocks, or mocks that f ## Frontend Tests -Coming Soon! +### Setup + +:::tip [Flarum CLI](https://github.com/flarum/cli) + +You can use the CLI to automatically add and update frontend testing infrastructure to your code: + +```bash +$ flarum-cli infra frontendTesting +``` + +::: + +First, you need to install the Jest config dev dependency: + +```bash +$ yarn add --dev @flarum/jest-config +``` + +Then, add the following to your `package.json`: + +```json +{ + "type": "module", + "scripts": { + ..., + "test": "yarn node --experimental-vm-modules $(yarn bin jest)" + } +} +``` + +Rename `webpack.config.js` to `webpack.config.cjs`. This is necessary because Jest doesn't support ESM yet. + +Create a `jest.config.cjs` file in the root of your extension: + +```js +module.exports = require('@flarum/jest-config')(); +``` + +If you are using TypeScript, create tsconfig.test.json with the following content: + +```json +{ + "extends": "./tsconfig.json", + "include": ["tests/**/*"], + "files": ["../../../node_modules/@flarum/jest-config/shims.d.ts"] +} +``` + +Then, you will need to set up a file structure for tests: + +``` +js +├── dist +├── src +├── tests +│ ├── unit +│ │ └── functionTest.test.js +│ ├── integration +│ │ └── componentTest.test.js +├── package.json +├── tsconfig.json +├── tsconfig.test.json +├── jest.config.cjs +└── webpack.config.cjs +``` + +#### Github Testing Workflow + +To run tests on every commit and pull request, check out the [GitHub Actions](github-actions.md) page. + +### Using Unit Tests + +Like any other JS project, you can use Jest to write unit tests for your frontend code. Checkout the [Jest docs](https://jestjs.io/docs/using-matchers) for more information on how to write tests. + +Here's a simple example of a unit test fo core's `abbreviateNumber` function: + +```ts +import abbreviateNumber from '../../../../src/common/utils/abbreviateNumber'; + +test('does not change small numbers', () => { + expect(abbreviateNumber(1)).toBe('1'); +}); + +test('abbreviates large numbers', () => { + expect(abbreviateNumber(1000000)).toBe('1M'); + expect(abbreviateNumber(100500)).toBe('100.5K'); +}); + +test('abbreviates large numbers with decimal places', () => { + expect(abbreviateNumber(100500)).toBe('100.5K'); + expect(abbreviateNumber(13234)).toBe('13.2K'); +}); +``` + +### Using Integration Tests + +Integration tests are used to test the components of your frontend code and the interaction between different components. For example, you might test that a page component renders the correct content based on certain parameters. + +Here's a simple example of an integration test for core's `Alert` component: + +```ts +import Alert from '../../../../src/common/components/Alert'; +import m from 'mithril'; +import mq from 'mithril-query'; +import { jest } from '@jest/globals'; + +describe('Alert displays as expected', () => { + it('should display alert messages with an icon', () => { + const alert = mq(m(Alert, { type: 'error' }, 'Shoot!')); + expect(alert).toContainRaw('Shoot!'); + expect(alert).toHaveElement('i.icon'); + }); + + it('should display alert messages with a custom icon when using a title', () => { + const alert = mq(Alert, { type: 'error', icon: 'fas fa-users', title: 'Woops..' }); + expect(alert).toContainRaw('Woops..'); + expect(alert).toHaveElement('i.fas.fa-users'); + }); + + it('should display alert messages with a title', () => { + const alert = mq(m(Alert, { type: 'error', title: 'Error Title' }, 'Shoot!')); + expect(alert).toContainRaw('Shoot!'); + expect(alert).toContainRaw('Error Title'); + expect(alert).toHaveElement('.Alert-title'); + }); + + it('should display alert messages with custom controls', () => { + const alert = mq(Alert, { type: 'error', controls: [m('button', { className: 'Button--test' }, 'Click me!')] }); + expect(alert).toHaveElement('button.Button--test'); + }); +}); + +describe('Alert is dismissible', () => { + it('should show dismiss button', function () { + const alert = mq(m(Alert, { dismissible: true }, 'Shoot!')); + expect(alert).toHaveElement('button.Alert-dismiss'); + }); + + it('should call ondismiss when dismiss button is clicked', function () { + const ondismiss = jest.fn(); + const alert = mq(Alert, { dismissible: true, ondismiss }); + alert.click('.Alert-dismiss'); + expect(ondismiss).toHaveBeenCalled(); + }); + + it('should not be dismissible if not chosen', function () { + const alert = mq(Alert, { type: 'error', dismissible: false }); + expect(alert).not.toHaveElement('button.Alert-dismiss'); + }); +}); +``` + +#### Methods + +These are the custom methods that are available for mithril component tests: +* **`toHaveElement(selector)`** - Checks if the component has an element that matches the given selector. +* **`toContainRaw(content)`** - Checks if the component HTML contains the given content. + +To negate any of these methods, simply prefix them with `not.`. For example, `expect(alert).not.toHaveElement('button.Alert-dismiss');`. For more information, check out the [Jest docs](https://jestjs.io/docs/using-matchers). For example you may need to check how to [mock functions](https://jestjs.io/docs/mock-functions), or how to use `beforeEach` and `afterEach` to set up and tear down tests. + + ## E2E Tests diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/extend/api.md b/i18n/zh/docusaurus-plugin-content-docs/current/extend/api.md index 367568967..63350767a 100644 --- a/i18n/zh/docusaurus-plugin-content-docs/current/extend/api.md +++ b/i18n/zh/docusaurus-plugin-content-docs/current/extend/api.md @@ -331,7 +331,7 @@ return [ (new Extend\ApiSerializer(UserSerializer::class)) // One attribute at a time ->attribute('firstName', function ($serializer, $user, $attributes) { - return $user->first_name + return $user->first_name }) // Multiple modifications at once, more complex logic ->mutate(function($serializer, $user, $attributes) { diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/extend/github-actions.md b/i18n/zh/docusaurus-plugin-content-docs/current/extend/github-actions.md index d810720cd..904bdf995 100644 --- a/i18n/zh/docusaurus-plugin-content-docs/current/extend/github-actions.md +++ b/i18n/zh/docusaurus-plugin-content-docs/current/extend/github-actions.md @@ -4,8 +4,6 @@ In public repos, [GitHub Actions](https://github.com/features/actions) allow you In this guide, you will learn how to add pre-defined workflows to your extension. -## Setup - :::tip [Flarum CLI](https://github.com/flarum/cli) You can use the CLI to automatically add and update workflows to your code: @@ -15,6 +13,9 @@ $ flarum-cli infra githubActions ::: +## Backend + + All you need to do is create a `.github/workflows/backend.yml` file in your extension, it will reuse a predefined workflow by the core development team which can be found [here](https://github.com/flarum/framework/blob/main/.github/workflows/REUSABLE_backend.yml). You need to specify the configuration as follows: ```yaml @@ -34,9 +35,7 @@ jobs: backend_directory: . ``` -## Backend - -Flarum provides a pre-defined workflow for running certain jobs for the backend of your extension. These are the currently available jobs: +These are the currently available jobs: | Name | Key | Description | | ----------------------------------------------- | ------------------------ | -------------------------------------- | @@ -69,4 +68,52 @@ For more details on parameters, [checkout the full predefined reusable workflow ## Frontend -Soon.. +All you need to do is create a `.github/workflows/frontend.yml` file in your extension, it will reuse a predefined workflow by the core development team which can be found [here](https://github.com/flarum/framework/blob/main/.github/workflows/REUSABLE_frontend.yml). You need to specify the configuration as follows: + +```yaml +name: Frontend + +on: [workflow_dispatch, push, pull_request] + +jobs: + run: + uses: flarum/framework/.github/workflows/REUSABLE_frontend.yml@main + with: + enable_bundlewatch: false + enable_prettier: true + enable_typescript: false + + frontend_directory: ./js + backend_directory: . + js_package_manager: yarn + main_git_branch: main + + secrets: + bundlewatch_github_token: ${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }} +``` + +Unlike the backend workflow, the frontend workflow runs everything in a single job. Here are the available parameters: + +| Name | Key | Description | Format | +| --------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| Build Script | `build_script` | Script to run for production build. Empty value to disable. | string | +| Build Typings Script | `build_typings_script` | Script to run for typings build. Empty value to disable. | string | +| Format Script | `format_script` | Script to run for code formatting. Empty value to disable. | string | +| Check Typings Script | `check_typings_script` | Script to run for tyiping check. Empty value to disable. | string | +| Type Coverage Script | `type_coverage_script` | Script to run for type coverage. Empty value to disable. | string | +| Test Script | `test_script` | Script to run for tests. Empty value to disable. | string | +| Enable Bundlewatch | `enable_bundlewatch` | Enable Bundlewatch? | string | +| Enable Prettier | `enable_prettier` | Enable Prettier? | string | +| Enable Typescript | `enable_typescript` | Enable TypeScript? | string | +| Enable Tests | `enable_tests` | Enable Tests? | string | +| Backend Directory | `backend_directory` | The directory of the project where backend code is located. This should contain a `composer.json` file, and is generally the root directory of the repo. | string | +| Frontend Directory | `frontend_directory` | The directory of the project where frontend code is located. This should contain a `package.json` file. | string | +| Main Git Branch | `main_git_branch` | The main git branch to use for the workflow. | string | +| Node Version | `node_version` | The node version to use for the workflow. | string | +| JS Package Manager | `js_package_manager` | The package manager to use (ex. yarn) | string | +| Cache Dependency Path | `cache_dependency_path` | The path to the cache dependency file. | string | + :::tip + +For more details on parameters, [checkout the full predefined reusable workflow file](https://github.com/flarum/framework/blob/main/.github/workflows/REUSABLE_frontend.yml). + +::: diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/extend/models.md b/i18n/zh/docusaurus-plugin-content-docs/current/extend/models.md index 822f354f3..5fecd6926 100644 --- a/i18n/zh/docusaurus-plugin-content-docs/current/extend/models.md +++ b/i18n/zh/docusaurus-plugin-content-docs/current/extend/models.md @@ -252,39 +252,40 @@ export default class Tag extends Model { } ``` -You must then register your new model with the store: +You must then register your new model with the store using the frontend `Store` extender in a new `extend.js` module: ```js -app.store.models.tags = Tag; +import Extend from 'flarum/common/extenders'; + +export default [ + new Extend.Store() + .add('tags', Tag), +]; ``` +:::info -<!-- You must then register your new model with the store using the `Model` extender: +Remember to export the `extend` module from your entry `index.js` file: ```js -export const extend = [ - new Extend.Model('tags', Tag) -]; -``` --> -### Extending Models -To add attributes and relationships to existing models, modify the model class prototype: - -```js -Discussion.prototype.user = Model.hasOne('user'); -Discussion.prototype.posts = Model.hasMany('posts'); -Discussion.prototype.slug = Model.attribute('slug'); +export { default as extend } from './extend'; ``` +::: -<!-- To add attributes and relationships to existing models, use the `Model` extender: +### Extending Models + +To add attributes and relationships to existing models, use the `Model` extender: + +```ts + new Extend.Model(Discussion) + .attribute<string>('slug') + .hasOne<User>('user') + .hasMany<Post>('posts') +``` -```js - new Extend.Model('discussions') - .attribute('slug') - .hasOne('user') - .hasMany('posts') -``` --> ### Saving Resources + To send data back through the API, call the `save` method on a model instance. This method returns a Promise which resolves with the same model instance: ```js diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/extend/routes.md b/i18n/zh/docusaurus-plugin-content-docs/current/extend/routes.md index 8a61803c6..a35bc38bf 100644 --- a/i18n/zh/docusaurus-plugin-content-docs/current/extend/routes.md +++ b/i18n/zh/docusaurus-plugin-content-docs/current/extend/routes.md @@ -160,37 +160,46 @@ On the backend, instead of adding your frontend route via the `Routes` extender, Now when `yourforum.com/users` is visited, the forum frontend will be displayed. However, since the frontend doesn't yet know about the `users` route, the discussion list will still be rendered. -Flarum builds on [Mithril's routing system](https://mithril.js.org/index.html#routing), adding route names and an abstract class for pages (`common/components/Page`). To register a new route, add an object for it to `app.routes`: +Flarum builds on [Mithril's routing system](https://mithril.js.org/index.html#routing), adding route names and an abstract class for pages (`common/components/Page`). -```js -app.routes['acme.users'] = { path: '/users', component: UsersPage }; -``` - - -<!-- To register the route on the frontend, there is a `Routes` extender which works much like the backend one. Instead of a controller, however, you pass a component instance as the third argument: +To register the route on the frontend, there is a `Routes` extender which works much like the backend one. Instead of a controller, however, you pass a component instance as the third argument: ```jsx -export const extend = [ +import Extend from 'flarum/common/extenders'; + +export default [ new Extend.Routes() - .add('/users', 'acme.users', <UsersPage />) + .add('acme.users', '/users', <UsersPage />), ]; -``` --> +``` + +:::info + +Remember to export the `extend` module from your entry `index.js` file: + +```js +export { default as extend } from './extend'; +``` + +::: + Now when `yourforum.com/users` is visited, the forum frontend will be loaded and the `UsersPage` component will be rendered in the content area. For more information on frontend pages, please see [that documentation section](frontend-pages.md). + Advanced use cases might also be interested in using [route resolvers](frontend-pages.md#route-resolvers-advanced). + ### Route Parameters -Frontend routes also allow you to capture segments of the URI, but the [Mithril route syntax](https://mithril.js.org/route.html) is slightly different: + +Frontend routes also allow you to capture segments of the URI: ```jsx -app.routes['acme.user'] = { path: '/user/:id', component: UserPage }; + new Extend.Routes() + .add('acme.user', '/user/:id', <UsersPage />) ``` - -<!-- ```jsx - new Extend.Routes() - .add('/user/:id', 'acme.user', <UsersPage />) -``` --> Route parameters will be passed into the `attrs` of the route's component. They will also be available through [`m.route.param`](https://mithril.js.org/route.html#mrouteparam) + ### Generating URLs + To generate a URL to a route on the frontend, use the `app.route` method. This accepts two arguments: the route name, and a hash of parameters. Parameters will fill in matching URI segments, otherwise they will be appended as query params. ```js @@ -198,6 +207,21 @@ const url = app.route('acme.user', { id: 123, foo: 'bar' }); // http://yourforum.com/users/123?foo=bar ``` +The extender also allows you to define a route helper method: + +```js + new Extend.Routes() + .add('acme.user', '/user/:id', <UsersPage />) + .helper('acmeUser', (user) => app.route('acme.user', { id: user.id() })) +``` + +This allows you to generate URLs to the route using the `acmeUser` helper method: + +```js +const url = app.route.acmeUser(user); +// http://yourforum.com/users/123 +``` + ### Linking to Other Pages A forum wouldn't be very useful if it only had one page. While you could, of course, implement links to other parts of your forum with HTML anchor tags and hardcoded links, this can be difficult to maintain, and defeats the purpose of Flarum being a [Single Page Application](https://en.wikipedia.org/wiki/Single-page_application) in the first place. diff --git a/i18n/zh/docusaurus-plugin-content-docs/current/extend/testing.md b/i18n/zh/docusaurus-plugin-content-docs/current/extend/testing.md index 3b427e82b..b1342ede0 100644 --- a/i18n/zh/docusaurus-plugin-content-docs/current/extend/testing.md +++ b/i18n/zh/docusaurus-plugin-content-docs/current/extend/testing.md @@ -18,8 +18,6 @@ $ flarum-cli infra backendTesting ::: -Firstly, you will need to require the `flarum/testing` composer package as a dev dependency for your extension: - ```bash composer require --dev flarum/testing:^1.0 ``` @@ -400,7 +398,167 @@ NOTE: If you find your extension needs _lots and lots_ of mocks, or mocks that f ## Frontend Tests -Coming Soon! +### Setup + +:::tip [Flarum CLI](https://github.com/flarum/cli) + +You can use the CLI to automatically add and update frontend testing infrastructure to your code: + +```bash +$ flarum-cli infra frontendTesting +``` + +::: + +First, you need to install the Jest config dev dependency: + +```bash +$ yarn add --dev @flarum/jest-config +``` + +Then, add the following to your `package.json`: + +```json +{ + "type": "module", + "scripts": { + ..., + "test": "yarn node --experimental-vm-modules $(yarn bin jest)" + } +} +``` + +Rename `webpack.config.js` to `webpack.config.cjs`. This is necessary because Jest doesn't support ESM yet. + +Create a `jest.config.cjs` file in the root of your extension: + +```js +module.exports = require('@flarum/jest-config')(); +``` + +If you are using TypeScript, create tsconfig.test.json with the following content: + +```json +{ + "extends": "./tsconfig.json", + "include": ["tests/**/*"], + "files": ["../../../node_modules/@flarum/jest-config/shims.d.ts"] +} +``` + +Then, you will need to set up a file structure for tests: + +``` +js +├── dist +├── src +├── tests +│ ├── unit +│ │ └── functionTest.test.js +│ ├── integration +│ │ └── componentTest.test.js +├── package.json +├── tsconfig.json +├── tsconfig.test.json +├── jest.config.cjs +└── webpack.config.cjs +``` + +#### GitHub Testing Workflow + +To run tests on every commit and pull request, check out the [GitHub Actions](github-actions.md) page. + +### Using Unit Tests + +Like any other JS project, you can use Jest to write unit tests for your frontend code. Checkout the [Jest docs](https://jestjs.io/docs/using-matchers) for more information on how to write tests. + +Here's a simple example of a unit test fo core's `abbreviateNumber` function: + +```ts +import abbreviateNumber from '../../../../src/common/utils/abbreviateNumber'; + +test('does not change small numbers', () => { + expect(abbreviateNumber(1)).toBe('1'); +}); + +test('abbreviates large numbers', () => { + expect(abbreviateNumber(1000000)).toBe('1M'); + expect(abbreviateNumber(100500)).toBe('100.5K'); +}); + +test('abbreviates large numbers with decimal places', () => { + expect(abbreviateNumber(100500)).toBe('100.5K'); + expect(abbreviateNumber(13234)).toBe('13.2K'); +}); +``` + +### Using Integration Tests + +Integration tests are used to test the components of your frontend code and the interaction between different components. For example, you might test that a page component renders the correct content based on certain parameters. + +Here's a simple example of an integration test for core's `Alert` component: + +```ts +import Alert from '../../../../src/common/components/Alert'; +import m from 'mithril'; +import mq from 'mithril-query'; +import { jest } from '@jest/globals'; + +describe('Alert displays as expected', () => { + it('should display alert messages with an icon', () => { + const alert = mq(m(Alert, { type: 'error' }, 'Shoot!')); + expect(alert).toContainRaw('Shoot!'); + expect(alert).toHaveElement('i.icon'); + }); + + it('should display alert messages with a custom icon when using a title', () => { + const alert = mq(Alert, { type: 'error', icon: 'fas fa-users', title: 'Woops..' }); + expect(alert).toContainRaw('Woops..'); + expect(alert).toHaveElement('i.fas.fa-users'); + }); + + it('should display alert messages with a title', () => { + const alert = mq(m(Alert, { type: 'error', title: 'Error Title' }, 'Shoot!')); + expect(alert).toContainRaw('Shoot!'); + expect(alert).toContainRaw('Error Title'); + expect(alert).toHaveElement('.Alert-title'); + }); + + it('should display alert messages with custom controls', () => { + const alert = mq(Alert, { type: 'error', controls: [m('button', { className: 'Button--test' }, 'Click me!')] }); + expect(alert).toHaveElement('button.Button--test'); + }); +}); + +describe('Alert is dismissible', () => { + it('should show dismiss button', function () { + const alert = mq(m(Alert, { dismissible: true }, 'Shoot!')); + expect(alert).toHaveElement('button.Alert-dismiss'); + }); + + it('should call ondismiss when dismiss button is clicked', function () { + const ondismiss = jest.fn(); + const alert = mq(Alert, { dismissible: true, ondismiss }); + alert.click('.Alert-dismiss'); + expect(ondismiss).toHaveBeenCalled(); + }); + + it('should not be dismissible if not chosen', function () { + const alert = mq(Alert, { type: 'error', dismissible: false }); + expect(alert).not.toHaveElement('button.Alert-dismiss'); + }); +}); +``` + +#### Methods + +These are the custom methods that are available for mithril component tests: +* **`toHaveElement(selector)`** - Checks if the component has an element that matches the given selector. +* **`toContainRaw(content)`** - Checks if the component HTML contains the given content. + +To negate any of these methods, simply prefix them with `not.`. For example, `expect(alert).not.toHaveElement('button.Alert-dismiss');`. For more information, check out the [Jest docs](https://jestjs.io/docs/using-matchers). For example you may need to check how to [mock functions](https://jestjs.io/docs/mock-functions), or how to use `beforeEach` and `afterEach` to set up and tear down tests. + + ## E2E Tests