mirror of
https://github.com/flarum/framework.git
synced 2024-11-22 09:48:15 +08:00
test: add frontend tests (#3991)
This commit is contained in:
parent
c0d3d976fa
commit
257be2b9db
4
.github/workflows/REUSABLE_frontend.yml
vendored
4
.github/workflows/REUSABLE_frontend.yml
vendored
|
@ -74,7 +74,7 @@ on:
|
||||||
description: The node version to use for the workflow.
|
description: The node version to use for the workflow.
|
||||||
type: number
|
type: number
|
||||||
required: false
|
required: false
|
||||||
default: 16
|
default: 20
|
||||||
|
|
||||||
js_package_manager:
|
js_package_manager:
|
||||||
description: "Enable TypeScript?"
|
description: "Enable TypeScript?"
|
||||||
|
@ -142,7 +142,7 @@ jobs:
|
||||||
working-directory: ${{ inputs.frontend_directory }}
|
working-directory: ${{ inputs.frontend_directory }}
|
||||||
|
|
||||||
- name: JS Checks & Production Build
|
- name: JS Checks & Production Build
|
||||||
uses: flarum/action-build@v3
|
uses: flarum/action-build@v4
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
build_script: ${{ inputs.build_script }}
|
build_script: ${{ inputs.build_script }}
|
||||||
|
|
|
@ -93,7 +93,7 @@ class ListTest extends TestCase
|
||||||
$data = json_decode($body, true);
|
$data = json_decode($body, true);
|
||||||
|
|
||||||
$tagIds = array_map(function ($tag) {
|
$tagIds = array_map(function ($tag) {
|
||||||
return $tag['id'];
|
return (int) $tag['id'];
|
||||||
}, array_filter($data['included'], function ($item) {
|
}, array_filter($data['included'], function ($item) {
|
||||||
return $item['type'] === 'tags';
|
return $item['type'] === 'tags';
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -221,6 +221,13 @@ export default class ExportRegistry implements IExportRegistry, IChunkRegistry {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public clear(): void {
|
||||||
|
this.moduleExports.clear();
|
||||||
|
this.onLoads.clear();
|
||||||
|
this.chunks.clear();
|
||||||
|
this.chunkModules.clear();
|
||||||
|
}
|
||||||
|
|
||||||
namespaceAndIdFromPath(path: string): [string, string] {
|
namespaceAndIdFromPath(path: string): [string, string] {
|
||||||
// Either we get a path like `flarum/forum/components/LogInModal` or `ext:flarum/tags/forum/components/TagPage`.
|
// Either we get a path like `flarum/forum/components/LogInModal` or `ext:flarum/tags/forum/components/TagPage`.
|
||||||
const matches = /^(?:ext:([^\/]+)\/(?:flarum-(?:ext-)?)?([^\/]+)|(flarum))(?:\/(.+))?$/.exec(path);
|
const matches = /^(?:ext:([^\/]+)\/(?:flarum-(?:ext-)?)?([^\/]+)|(flarum))(?:\/(.+))?$/.exec(path);
|
||||||
|
|
|
@ -36,7 +36,7 @@ export default class Avatar<CustomAttrs extends IAvatarAttrs = IAvatarAttrs> ext
|
||||||
}
|
}
|
||||||
|
|
||||||
content = username.charAt(0).toUpperCase();
|
content = username.charAt(0).toUpperCase();
|
||||||
attrs.style = { '--avatar-bg': user.color() };
|
attrs.style = !process.env.testing && { '--avatar-bg': user.color() };
|
||||||
}
|
}
|
||||||
|
|
||||||
return <span {...attrs}>{content}</span>;
|
return <span {...attrs}>{content}</span>;
|
||||||
|
|
|
@ -32,7 +32,11 @@ export default class Badge<CustomAttrs extends IBadgeAttrs = IBadgeAttrs> extend
|
||||||
|
|
||||||
const iconChild = iconName ? <Icon name={iconName} className="Badge-icon" /> : m.trust(' ');
|
const iconChild = iconName ? <Icon name={iconName} className="Badge-icon" /> : m.trust(' ');
|
||||||
|
|
||||||
const newStyle = { ...style, '--badge-bg': color };
|
const newStyle = { ...style };
|
||||||
|
|
||||||
|
if (!process.env.testing) {
|
||||||
|
newStyle['--badge-bg'] = color;
|
||||||
|
}
|
||||||
|
|
||||||
const badgeAttrs = {
|
const badgeAttrs = {
|
||||||
...attrs,
|
...attrs,
|
||||||
|
|
|
@ -1,13 +1,22 @@
|
||||||
import type Mithril from 'mithril';
|
import type Mithril from 'mithril';
|
||||||
|
|
||||||
import Component, { ComponentAttrs } from '../Component';
|
import Component, { ComponentAttrs } from '../Component';
|
||||||
import classList from '../utils/classList';
|
import classList from '../utils/classList';
|
||||||
|
|
||||||
import Icon from './Icon';
|
import Icon from './Icon';
|
||||||
|
|
||||||
export default class ColorPreviewInput extends Component {
|
export interface IColorPreviewInputAttrs extends ComponentAttrs {
|
||||||
view(vnode: Mithril.Vnode<ComponentAttrs, this>) {
|
value: string;
|
||||||
const { className, id, ...attrs } = this.attrs;
|
id?: string;
|
||||||
|
type?: string;
|
||||||
|
onchange?: (event: { target: { value: string } }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ColorPreviewInput<
|
||||||
|
CustomAttributes extends IColorPreviewInputAttrs = IColorPreviewInputAttrs
|
||||||
|
> extends Component<CustomAttributes> {
|
||||||
|
view(vnode: Mithril.Vnode<CustomAttributes, this>) {
|
||||||
|
const { className, id, ...otherAttrs } = this.attrs;
|
||||||
|
|
||||||
|
const attrs = otherAttrs as unknown as IColorPreviewInputAttrs;
|
||||||
|
|
||||||
attrs.type ||= 'text';
|
attrs.type ||= 'text';
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import Component, { ComponentAttrs } from '../Component';
|
import Component, { ComponentAttrs } from '../Component';
|
||||||
import listItems from '../helpers/listItems';
|
|
||||||
import classList from '../utils/classList';
|
import classList from '../utils/classList';
|
||||||
import Mithril from 'mithril';
|
import Mithril from 'mithril';
|
||||||
|
|
||||||
|
|
|
@ -74,14 +74,14 @@ const StackedFormControlType = 'stacked-text' as const;
|
||||||
* Valid options for the setting component builder to generate a Switch.
|
* Valid options for the setting component builder to generate a Switch.
|
||||||
*/
|
*/
|
||||||
export interface SwitchFieldComponentOptions extends CommonFieldOptions {
|
export interface SwitchFieldComponentOptions extends CommonFieldOptions {
|
||||||
type: typeof BooleanSettingTypes[number];
|
type: (typeof BooleanSettingTypes)[number];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Valid options for the setting component builder to generate a Select dropdown.
|
* Valid options for the setting component builder to generate a Select dropdown.
|
||||||
*/
|
*/
|
||||||
export interface SelectFieldComponentOptions extends CommonFieldOptions {
|
export interface SelectFieldComponentOptions extends CommonFieldOptions {
|
||||||
type: typeof SelectSettingTypes[number] | typeof RadioSettingTypes[number];
|
type: (typeof SelectSettingTypes)[number] | (typeof RadioSettingTypes)[number];
|
||||||
/**
|
/**
|
||||||
* Map of values to their labels
|
* Map of values to their labels
|
||||||
*/
|
*/
|
||||||
|
@ -101,7 +101,7 @@ export interface SelectFieldComponentOptions extends CommonFieldOptions {
|
||||||
* Valid options for the setting component builder to generate a Textarea.
|
* Valid options for the setting component builder to generate a Textarea.
|
||||||
*/
|
*/
|
||||||
export interface TextareaFieldComponentOptions extends CommonFieldOptions {
|
export interface TextareaFieldComponentOptions extends CommonFieldOptions {
|
||||||
type: typeof TextareaSettingTypes[number];
|
type: (typeof TextareaSettingTypes)[number];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -246,7 +246,7 @@ export default class FormGroup<CustomAttrs extends IFormGroupAttrs = IFormGroupA
|
||||||
if ((TextareaSettingTypes as readonly string[]).includes(type)) {
|
if ((TextareaSettingTypes as readonly string[]).includes(type)) {
|
||||||
settingElement = <textarea id={inputId} aria-describedby={helpTextId} bidi={stream} {...attrs} />;
|
settingElement = <textarea id={inputId} aria-describedby={helpTextId} bidi={stream} {...attrs} />;
|
||||||
} else {
|
} else {
|
||||||
let Tag: VnodeElementTag = 'input';
|
let Tag: VnodeElementTag | typeof ColorPreviewInput = 'input';
|
||||||
|
|
||||||
if (type === ColorPreviewSettingType) {
|
if (type === ColorPreviewSettingType) {
|
||||||
Tag = ColorPreviewInput;
|
Tag = ColorPreviewInput;
|
||||||
|
|
|
@ -79,9 +79,6 @@ export default abstract class Modal<ModalAttrs extends IInternalModalAttrs = IIn
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @todo split into FormModal and Modal in 2.0
|
|
||||||
*/
|
|
||||||
view() {
|
view() {
|
||||||
if (this.alertAttrs) {
|
if (this.alertAttrs) {
|
||||||
this.alertAttrs.dismissible = false;
|
this.alertAttrs.dismissible = false;
|
||||||
|
|
|
@ -43,7 +43,7 @@ export default class ModalManager extends Component<IModalManagerAttrs> {
|
||||||
data-modal-number={i}
|
data-modal-number={i}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
style={{ '--modal-number': i }}
|
style={!process.env.testing && { '--modal-number': i }}
|
||||||
aria-hidden={this.attrs.state.modal !== modal && 'true'}
|
aria-hidden={this.attrs.state.modal !== modal && 'true'}
|
||||||
>
|
>
|
||||||
{!!Tag && [
|
{!!Tag && [
|
||||||
|
@ -66,7 +66,7 @@ export default class ModalManager extends Component<IModalManagerAttrs> {
|
||||||
className="Modal-backdrop backdrop"
|
className="Modal-backdrop backdrop"
|
||||||
ontransitionend={this.onBackdropTransitionEnd.bind(this)}
|
ontransitionend={this.onBackdropTransitionEnd.bind(this)}
|
||||||
data-showing={!!this.attrs.state.modalList.length || this.attrs.state.loadingModal}
|
data-showing={!!this.attrs.state.modalList.length || this.attrs.state.loadingModal}
|
||||||
style={{ '--modal-count': this.attrs.state.modalList.length + Number(this.attrs.state.loadingModal) }}
|
style={!process.env.testing && { '--modal-count': this.attrs.state.modalList.length + Number(this.attrs.state.loadingModal) }}
|
||||||
>
|
>
|
||||||
{this.attrs.state.loadingModal && <LoadingIndicator />}
|
{this.attrs.state.loadingModal && <LoadingIndicator />}
|
||||||
</div>
|
</div>
|
||||||
|
@ -122,7 +122,9 @@ export default class ModalManager extends Component<IModalManagerAttrs> {
|
||||||
this.focusTrap = createFocusTrap(this.activeDialogElement as HTMLElement, { allowOutsideClick: true });
|
this.focusTrap = createFocusTrap(this.activeDialogElement as HTMLElement, { allowOutsideClick: true });
|
||||||
this.focusTrap!.activate?.();
|
this.focusTrap!.activate?.();
|
||||||
|
|
||||||
disableBodyScroll(this.activeDialogManagerElement!, { reserveScrollBarGap: true });
|
if (this.activeDialogManagerElement) {
|
||||||
|
disableBodyScroll(this.activeDialogManagerElement, { reserveScrollBarGap: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update key of current opened modal
|
// Update key of current opened modal
|
||||||
|
@ -136,21 +138,21 @@ export default class ModalManager extends Component<IModalManagerAttrs> {
|
||||||
/**
|
/**
|
||||||
* Get current active dialog
|
* Get current active dialog
|
||||||
*/
|
*/
|
||||||
private get activeDialogElement(): HTMLElement {
|
private get activeDialogElement(): HTMLElement | null {
|
||||||
return document.body.querySelector(`.ModalManager[data-modal-key="${this.attrs.state.modal?.key}"] .Modal`) as HTMLElement;
|
return document.body.querySelector(`.ModalManager[data-modal-key="${this.attrs.state.modal?.key}"] .Modal`) as HTMLElement | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current active dialog
|
* Get current active dialog
|
||||||
*/
|
*/
|
||||||
private get activeDialogManagerElement(): HTMLElement {
|
private get activeDialogManagerElement(): HTMLElement | null {
|
||||||
return document.body.querySelector(`.ModalManager[data-modal-key="${this.attrs.state.modal?.key}"]`) as HTMLElement;
|
return document.body.querySelector(`.ModalManager[data-modal-key="${this.attrs.state.modal?.key}"]`) as HTMLElement | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
animateShow(readyCallback: () => void = () => {}): void {
|
animateShow(readyCallback: () => void = () => {}): void {
|
||||||
if (!this.attrs.state.modal) return;
|
if (!this.attrs.state.modal) return;
|
||||||
|
|
||||||
this.activeDialogElement.addEventListener(
|
this.activeDialogElement?.addEventListener(
|
||||||
'transitionend',
|
'transitionend',
|
||||||
() => {
|
() => {
|
||||||
readyCallback();
|
readyCallback();
|
||||||
|
@ -159,7 +161,7 @@ export default class ModalManager extends Component<IModalManagerAttrs> {
|
||||||
);
|
);
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
this.activeDialogElement.classList.add('in');
|
this.activeDialogElement?.classList.add('in');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,10 +178,10 @@ export default class ModalManager extends Component<IModalManagerAttrs> {
|
||||||
closedCallback();
|
closedCallback();
|
||||||
};
|
};
|
||||||
|
|
||||||
this.activeDialogElement.addEventListener('transitionend', afterModalClosedCallback, { once: true });
|
this.activeDialogElement?.addEventListener('transitionend', afterModalClosedCallback, { once: true });
|
||||||
|
|
||||||
this.activeDialogElement.classList.remove('in');
|
this.activeDialogElement?.classList.remove('in');
|
||||||
this.activeDialogElement.classList.add('out');
|
this.activeDialogElement?.classList.add('out');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected handleEscPress(e: KeyboardEvent): void {
|
protected handleEscPress(e: KeyboardEvent): void {
|
||||||
|
|
|
@ -1,38 +1,14 @@
|
||||||
import app from '../app';
|
import app from '../app';
|
||||||
import Component, { type ComponentAttrs } from '../Component';
|
|
||||||
import classList from '../utils/classList';
|
import classList from '../utils/classList';
|
||||||
import Dropdown from './Dropdown';
|
import Dropdown from './Dropdown';
|
||||||
import Mithril from 'mithril';
|
import Mithril from 'mithril';
|
||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
import Tooltip from './Tooltip';
|
import Tooltip from './Tooltip';
|
||||||
|
import Select, { ISelectAttrs, Option } from './Select';
|
||||||
|
|
||||||
export type Option = {
|
export interface IMultiSelectAttrs extends ISelectAttrs {}
|
||||||
label: string;
|
|
||||||
disabled?: boolean | ((value: string[]) => boolean);
|
|
||||||
tooltip?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface IMultiSelectAttrs extends ComponentAttrs {
|
export default class MultiSelect<CustomAttrs extends IMultiSelectAttrs = IMultiSelectAttrs> extends Select<CustomAttrs> {
|
||||||
options: Record<string, string | Option>;
|
|
||||||
onchange?: (value: string[]) => void;
|
|
||||||
value?: string[];
|
|
||||||
disabled?: boolean;
|
|
||||||
wrapperAttrs?: Record<string, string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `MultiSelect` component displays an input with selected elements.
|
|
||||||
* With a dropdown to select multiple options.
|
|
||||||
*
|
|
||||||
* - `options` A map of option values to labels.
|
|
||||||
* - `onchange` A callback to run when the selected value is changed.
|
|
||||||
* - `value` The value of the selected option.
|
|
||||||
* - `disabled` Disabled state for the input.
|
|
||||||
* - `wrapperAttrs` A map of attrs to be passed to the DOM element wrapping the input.
|
|
||||||
*
|
|
||||||
* Other attributes are passed directly to the input element rendered to the DOM.
|
|
||||||
*/
|
|
||||||
export default class MultiSelect<CustomAttrs extends IMultiSelectAttrs = IMultiSelectAttrs> extends Component<CustomAttrs> {
|
|
||||||
protected selected: string[] = [];
|
protected selected: string[] = [];
|
||||||
|
|
||||||
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||||
|
@ -41,7 +17,7 @@ export default class MultiSelect<CustomAttrs extends IMultiSelectAttrs = IMultiS
|
||||||
this.selected = this.attrs.value || [];
|
this.selected = this.attrs.value || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
view() {
|
input(): JSX.Element {
|
||||||
const {
|
const {
|
||||||
options,
|
options,
|
||||||
onchange,
|
onchange,
|
||||||
|
@ -49,15 +25,10 @@ export default class MultiSelect<CustomAttrs extends IMultiSelectAttrs = IMultiS
|
||||||
className,
|
className,
|
||||||
class: _class,
|
class: _class,
|
||||||
|
|
||||||
// Destructure the `wrapperAttrs` object to extract the `className` for passing to `classList()`
|
|
||||||
// `= {}` prevents errors when `wrapperAttrs` is undefined
|
|
||||||
wrapperAttrs: { className: wrapperClassName, class: wrapperClass, ...wrapperAttrs } = {},
|
|
||||||
|
|
||||||
...domAttrs
|
...domAttrs
|
||||||
} = this.attrs;
|
} = this.attrs;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={classList('Select MultiSelect', wrapperClassName, wrapperClass)} {...wrapperAttrs}>
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
buttonClassName="Button"
|
buttonClassName="Button"
|
||||||
|
@ -82,7 +53,7 @@ export default class MultiSelect<CustomAttrs extends IMultiSelectAttrs = IMultiS
|
||||||
const button = (
|
const button = (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className={classList('Dropdown-item', { disabled })}
|
className={classList('Dropdown-item', `Dropdown-item--${key}`, { disabled })}
|
||||||
onclick={this.toggle.bind(this, key)}
|
onclick={this.toggle.bind(this, key)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
icon={this.selected.includes(key) ? 'fas fa-check' : 'fas fa-empty'}
|
icon={this.selected.includes(key) ? 'fas fa-check' : 'fas fa-empty'}
|
||||||
|
@ -98,7 +69,6 @@ export default class MultiSelect<CustomAttrs extends IMultiSelectAttrs = IMultiS
|
||||||
return button;
|
return button;
|
||||||
})}
|
})}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</span>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,22 @@
|
||||||
import Component from '../Component';
|
import Component, { type ComponentAttrs } from '../Component';
|
||||||
import withAttr from '../utils/withAttr';
|
import withAttr from '../utils/withAttr';
|
||||||
import classList from '../utils/classList';
|
import classList from '../utils/classList';
|
||||||
import Icon from './Icon';
|
import Icon from './Icon';
|
||||||
|
|
||||||
|
export type Option = {
|
||||||
|
label: string;
|
||||||
|
disabled?: boolean | ((value: any) => boolean);
|
||||||
|
tooltip?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ISelectAttrs extends ComponentAttrs {
|
||||||
|
options: Record<string, string | Option>;
|
||||||
|
onchange?: (value: any) => void;
|
||||||
|
value?: any;
|
||||||
|
disabled?: boolean;
|
||||||
|
wrapperAttrs?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `Select` component displays a <select> input, surrounded with some extra
|
* The `Select` component displays a <select> input, surrounded with some extra
|
||||||
* elements for styling. It accepts the following attrs:
|
* elements for styling. It accepts the following attrs:
|
||||||
|
@ -15,8 +29,22 @@ import Icon from './Icon';
|
||||||
*
|
*
|
||||||
* Other attributes are passed directly to the `<select>` element rendered to the DOM.
|
* Other attributes are passed directly to the `<select>` element rendered to the DOM.
|
||||||
*/
|
*/
|
||||||
export default class Select extends Component {
|
export default class Select<CustomAttrs extends ISelectAttrs = ISelectAttrs> extends Component<CustomAttrs> {
|
||||||
view() {
|
view() {
|
||||||
|
const {
|
||||||
|
// Destructure the `wrapperAttrs` object to extract the `className` for passing to `classList()`
|
||||||
|
// `= {}` prevents errors when `wrapperAttrs` is undefined
|
||||||
|
wrapperAttrs: { className: wrapperClassName, class: wrapperClass, ...wrapperAttrs } = {},
|
||||||
|
} = this.attrs;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={classList('Select', wrapperClassName, wrapperClass)} {...wrapperAttrs}>
|
||||||
|
{this.input()}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
input() {
|
||||||
const {
|
const {
|
||||||
options,
|
options,
|
||||||
onchange,
|
onchange,
|
||||||
|
@ -25,15 +53,11 @@ export default class Select extends Component {
|
||||||
className,
|
className,
|
||||||
class: _class,
|
class: _class,
|
||||||
|
|
||||||
// Destructure the `wrapperAttrs` object to extract the `className` for passing to `classList()`
|
|
||||||
// `= {}` prevents errors when `wrapperAttrs` is undefined
|
|
||||||
wrapperAttrs: { className: wrapperClassName, class: wrapperClass, ...wrapperAttrs } = {},
|
|
||||||
|
|
||||||
...domAttrs
|
...domAttrs
|
||||||
} = this.attrs;
|
} = this.attrs;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={classList('Select', wrapperClassName, wrapperClass)} {...wrapperAttrs}>
|
<>
|
||||||
<select
|
<select
|
||||||
className={classList('Select-input FormControl', className, _class)}
|
className={classList('Select-input FormControl', className, _class)}
|
||||||
onchange={onchange ? withAttr('value', onchange.bind(this)) : undefined}
|
onchange={onchange ? withAttr('value', onchange.bind(this)) : undefined}
|
||||||
|
@ -43,15 +67,11 @@ export default class Select extends Component {
|
||||||
>
|
>
|
||||||
{Object.keys(options).map((key) => {
|
{Object.keys(options).map((key) => {
|
||||||
const option = options[key];
|
const option = options[key];
|
||||||
|
const label = typeof option === 'object' && 'label' in option ? option.label : option;
|
||||||
|
let disabled = typeof option === 'object' && 'disabled' in option ? option.disabled : false;
|
||||||
|
|
||||||
let label;
|
if (typeof disabled === 'function') {
|
||||||
let disabled = false;
|
disabled = disabled(value ?? null);
|
||||||
|
|
||||||
if (typeof option === 'object' && option.label) {
|
|
||||||
label = option.label;
|
|
||||||
disabled = option.disabled ?? false;
|
|
||||||
} else {
|
|
||||||
label = option;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -62,7 +82,7 @@ export default class Select extends Component {
|
||||||
})}
|
})}
|
||||||
</select>
|
</select>
|
||||||
<Icon name="fas fa-sort" className="Select-caret" />
|
<Icon name="fas fa-sort" className="Select-caret" />
|
||||||
</span>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -35,7 +35,7 @@ export function slug(string: string, mode: SluggingMode = SluggingMode.ALPHANUME
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SluggingMode {
|
export enum SluggingMode {
|
||||||
ALPHANUMERIC = 'alphanum',
|
ALPHANUMERIC = 'alphanum',
|
||||||
UTF8 = 'utf8',
|
UTF8 = 'utf8',
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import ItemList from '../../common/utils/ItemList';
|
||||||
import Tooltip from '../../common/components/Tooltip';
|
import Tooltip from '../../common/components/Tooltip';
|
||||||
import HeaderList from './HeaderList';
|
import HeaderList from './HeaderList';
|
||||||
import HeaderListGroup from './HeaderListGroup';
|
import HeaderListGroup from './HeaderListGroup';
|
||||||
|
import NotificationType from './NotificationType';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `NotificationList` component displays a list of the logged-in user's
|
* The `NotificationList` component displays a list of the logged-in user's
|
||||||
|
@ -116,12 +117,7 @@ export default class NotificationList extends Component {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{group.notifications
|
{group.notifications.map((notification) => <NotificationType notification={notification} />).filter((component) => !!component)}
|
||||||
.map((notification) => {
|
|
||||||
const NotificationComponent = app.notificationComponents[notification.contentType()];
|
|
||||||
return !!NotificationComponent ? <NotificationComponent notification={notification} /> : null;
|
|
||||||
})
|
|
||||||
.filter((component) => !!component)}
|
|
||||||
</HeaderListGroup>
|
</HeaderListGroup>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
16
framework/core/js/src/forum/components/NotificationType.tsx
Normal file
16
framework/core/js/src/forum/components/NotificationType.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import Component, { type ComponentAttrs } from '../../common/Component';
|
||||||
|
import type Notification from '../../common/models/Notification';
|
||||||
|
import app from '../app';
|
||||||
|
|
||||||
|
export interface INotificationTypeAttrs extends ComponentAttrs {
|
||||||
|
notification: Notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class NotificationType<CustomAttrs extends INotificationTypeAttrs = INotificationTypeAttrs> extends Component<CustomAttrs> {
|
||||||
|
view() {
|
||||||
|
const notification = this.attrs.notification;
|
||||||
|
const NotificationComponent = app.notificationComponents[notification.contentType()!];
|
||||||
|
|
||||||
|
return !!NotificationComponent ? <NotificationComponent notification={notification} /> : null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import LoadingPost from './LoadingPost';
|
||||||
import ReplyPlaceholder from './ReplyPlaceholder';
|
import ReplyPlaceholder from './ReplyPlaceholder';
|
||||||
import Button from '../../common/components/Button';
|
import Button from '../../common/components/Button';
|
||||||
import ItemList from '../../common/utils/ItemList';
|
import ItemList from '../../common/utils/ItemList';
|
||||||
|
import PostType from './PostType';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `PostStream` component displays an infinitely-scrollable wall of posts in
|
* The `PostStream` component displays an infinitely-scrollable wall of posts in
|
||||||
|
@ -47,8 +48,7 @@ export default class PostStream extends Component {
|
||||||
|
|
||||||
if (post) {
|
if (post) {
|
||||||
const time = post.createdAt();
|
const time = post.createdAt();
|
||||||
const PostComponent = app.postComponents[post.contentType()];
|
content = <PostType post={post} />;
|
||||||
content = !!PostComponent && <PostComponent post={post} />;
|
|
||||||
|
|
||||||
attrs.key = 'post' + post.id();
|
attrs.key = 'post' + post.id();
|
||||||
attrs.oncreate = postFadeIn;
|
attrs.oncreate = postFadeIn;
|
||||||
|
|
16
framework/core/js/src/forum/components/PostType.tsx
Normal file
16
framework/core/js/src/forum/components/PostType.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import Component, { type ComponentAttrs } from '../../common/Component';
|
||||||
|
import type Post from '../../common/models/Post';
|
||||||
|
import app from '../app';
|
||||||
|
|
||||||
|
export interface IPostTypeAttrs extends ComponentAttrs {
|
||||||
|
post: Post;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class PostType<CustomAttrs extends IPostTypeAttrs = IPostTypeAttrs> extends Component<CustomAttrs> {
|
||||||
|
view() {
|
||||||
|
const post = this.attrs.post;
|
||||||
|
const PostComponent = app.postComponents[post.contentType()!];
|
||||||
|
|
||||||
|
return !!PostComponent && <PostComponent post={post} />;
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,7 +30,7 @@ export default class UserCard extends Component {
|
||||||
const color = user.color();
|
const color = user.color();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classList('UserCard', this.attrs.className)} style={color && { '--usercard-bg': color }}>
|
<div className={classList('UserCard', this.attrs.className)} style={color && !process.env.testing && { '--usercard-bg': color }}>
|
||||||
<div className="darkenBackground">
|
<div className="darkenBackground">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="UserCard-profile">{this.profileItems().toArray()}</div>
|
<div className="UserCard-profile">{this.profileItems().toArray()}</div>
|
||||||
|
|
58
framework/core/js/tests/factory.ts
Normal file
58
framework/core/js/tests/factory.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
export function makeDiscussion(data: any = {}) {
|
||||||
|
return makeModel('discussions', data.id, {
|
||||||
|
attributes: {
|
||||||
|
title: 'Discussion 1',
|
||||||
|
slug: 'discussion-1',
|
||||||
|
lastPostedAt: new Date().toISOString(),
|
||||||
|
unreadCount: 0,
|
||||||
|
commentCount: 0,
|
||||||
|
...(data.attributes || {}),
|
||||||
|
},
|
||||||
|
relationships: {
|
||||||
|
firstPost: {
|
||||||
|
data: { type: 'posts', id: '1' },
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
data: { type: 'users', id: '1' },
|
||||||
|
},
|
||||||
|
...(data.relationships || {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeUser(data: any = {}) {
|
||||||
|
return makeModel('users', data.id, {
|
||||||
|
attributes: {
|
||||||
|
id: data.id,
|
||||||
|
username: 'user' + data.id,
|
||||||
|
displayName: 'User ' + data.id,
|
||||||
|
email: `user${data.id}@machine.local`,
|
||||||
|
joinTime: '2021-01-01T00:00:00Z',
|
||||||
|
isEmailConfirmed: true,
|
||||||
|
preferences: {},
|
||||||
|
...(data.attributes || {}),
|
||||||
|
},
|
||||||
|
relationships: {
|
||||||
|
groups: {
|
||||||
|
data: [],
|
||||||
|
},
|
||||||
|
...(data.relationships || {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeModel(type: string | undefined | null, id: string | number | undefined | null, data: any) {
|
||||||
|
if (!id) {
|
||||||
|
throw new Error('You must provide an id when making a model');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!type) {
|
||||||
|
throw new Error('You must provide a type when making a model');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
id,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import bootstrapAdmin from '@flarum/jest-config/src/boostrap/admin';
|
||||||
|
import AdvancedPage from '../../../../src/admin/components/AdvancedPage';
|
||||||
|
import { app } from '../../../../src/admin';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapAdmin());
|
||||||
|
|
||||||
|
describe('AdvancedPage', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
app.boot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders', () => {
|
||||||
|
const page = mq(AdvancedPage);
|
||||||
|
|
||||||
|
expect(page).toHaveElement('.AdvancedPage');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,18 @@
|
||||||
|
import bootstrapAdmin from '@flarum/jest-config/src/boostrap/admin';
|
||||||
|
import AppearancePage from '../../../../src/admin/components/AppearancePage';
|
||||||
|
import { app } from '../../../../src/admin';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapAdmin());
|
||||||
|
|
||||||
|
describe('AppearancePage', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
app.boot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders', () => {
|
||||||
|
const page = mq(AppearancePage);
|
||||||
|
|
||||||
|
expect(page).toHaveElement('.AppearancePage');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,18 @@
|
||||||
|
import bootstrapAdmin from '@flarum/jest-config/src/boostrap/admin';
|
||||||
|
import BasicsPage from '../../../../src/admin/components/BasicsPage';
|
||||||
|
import { app } from '../../../../src/admin';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapAdmin());
|
||||||
|
|
||||||
|
describe('BasicsPage', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
app.boot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders', () => {
|
||||||
|
const page = mq(BasicsPage);
|
||||||
|
|
||||||
|
expect(page).toHaveElement('.BasicsPage');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,18 @@
|
||||||
|
import bootstrapAdmin from '@flarum/jest-config/src/boostrap/admin';
|
||||||
|
import DashboardPage from '../../../../src/admin/components/DashboardPage';
|
||||||
|
import { app } from '../../../../src/admin';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapAdmin());
|
||||||
|
|
||||||
|
describe('DashboardPage', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
app.boot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders', () => {
|
||||||
|
const page = mq(DashboardPage);
|
||||||
|
|
||||||
|
expect(page).toHaveElement('.DashboardPage');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,18 @@
|
||||||
|
import bootstrapAdmin from '@flarum/jest-config/src/boostrap/admin';
|
||||||
|
import MailPage from '../../../../src/admin/components/MailPage';
|
||||||
|
import { app } from '../../../../src/admin';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapAdmin());
|
||||||
|
|
||||||
|
describe('MailPage', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
app.boot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders', () => {
|
||||||
|
const page = mq(MailPage);
|
||||||
|
|
||||||
|
expect(page).toHaveElement('.MailPage');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,37 @@
|
||||||
|
import bootstrapAdmin from '@flarum/jest-config/src/boostrap/admin';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
import { app } from '../../../../src/admin';
|
||||||
|
import ModalManager from '../../../../src/common/components/ModalManager';
|
||||||
|
import CreateUserModal from '../../../../src/admin/components/CreateUserModal';
|
||||||
|
import EditCustomCssModal from '../../../../src/admin/components/EditCustomCssModal';
|
||||||
|
import EditCustomFooterModal from '../../../../src/admin/components/EditCustomFooterModal';
|
||||||
|
import EditCustomHeaderModal from '../../../../src/admin/components/EditCustomHeaderModal';
|
||||||
|
import EditGroupModal from '../../../../src/admin/components/EditGroupModal';
|
||||||
|
import LoadingModal from '../../../../src/admin/components/LoadingModal';
|
||||||
|
import ReadmeModal from '../../../../src/admin/components/ReadmeModal';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapAdmin());
|
||||||
|
|
||||||
|
describe('Modals', () => {
|
||||||
|
beforeAll(() => app.boot());
|
||||||
|
beforeEach(() => app.modal.close());
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
[CreateUserModal, {}],
|
||||||
|
[EditCustomCssModal, {}],
|
||||||
|
[EditCustomFooterModal, {}],
|
||||||
|
[EditCustomHeaderModal, {}],
|
||||||
|
[EditGroupModal, {}],
|
||||||
|
[LoadingModal, {}],
|
||||||
|
[ReadmeModal, { extension: { id: 'flarum-test', extra: { 'flarum-extension': { title: 'Test' } } } }],
|
||||||
|
])('modal renders', (modal, attrs) => {
|
||||||
|
const manager = mq(ModalManager, { state: app.modal });
|
||||||
|
|
||||||
|
app.modal.show(modal, attrs || {});
|
||||||
|
|
||||||
|
manager.redraw();
|
||||||
|
|
||||||
|
expect(app.modal.modalList.length).toEqual(1);
|
||||||
|
expect(manager).toHaveElement('.ModalManager');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,18 @@
|
||||||
|
import bootstrapAdmin from '@flarum/jest-config/src/boostrap/admin';
|
||||||
|
import PermissionsPage from '../../../../src/admin/components/PermissionsPage';
|
||||||
|
import { app } from '../../../../src/admin';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapAdmin());
|
||||||
|
|
||||||
|
describe('PermissionsPage', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
app.boot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders', () => {
|
||||||
|
const page = mq(PermissionsPage);
|
||||||
|
|
||||||
|
expect(page).toHaveElement('.PermissionsPage');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,18 @@
|
||||||
|
import bootstrapAdmin from '@flarum/jest-config/src/boostrap/admin';
|
||||||
|
import UserListPage from '../../../../src/admin/components/UserListPage';
|
||||||
|
import { app } from '../../../../src/admin';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapAdmin());
|
||||||
|
|
||||||
|
describe('UserListPage', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
app.boot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders', () => {
|
||||||
|
const page = mq(UserListPage);
|
||||||
|
|
||||||
|
expect(page).toHaveElement('.UserListPage');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,45 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import { app } from '../../../src/forum';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
import AlertManager from '../../../src/common/components/AlertManager';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('AlertManager', () => {
|
||||||
|
beforeAll(() => app.boot());
|
||||||
|
|
||||||
|
test('can show and dismiss an alert', () => {
|
||||||
|
const manager = mq(AlertManager, { state: app.alerts });
|
||||||
|
|
||||||
|
const id = app.alerts.show({ type: 'success' }, 'Hello, world!');
|
||||||
|
|
||||||
|
manager.redraw();
|
||||||
|
|
||||||
|
expect(manager).toContainRaw('Hello, world!');
|
||||||
|
|
||||||
|
app.alerts.dismiss(id);
|
||||||
|
|
||||||
|
manager.redraw();
|
||||||
|
|
||||||
|
expect(manager).not.toContainRaw('Hello, world!');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can clear all alerts', () => {
|
||||||
|
const manager = mq(AlertManager, { state: app.alerts });
|
||||||
|
|
||||||
|
app.alerts.show({ type: 'success' }, 'Hello, world!');
|
||||||
|
app.alerts.show({ type: 'error' }, 'Goodbye, world!');
|
||||||
|
|
||||||
|
manager.redraw();
|
||||||
|
|
||||||
|
expect(manager).toContainRaw('Hello, world!');
|
||||||
|
expect(manager).toContainRaw('Goodbye, world!');
|
||||||
|
|
||||||
|
app.alerts.clear();
|
||||||
|
|
||||||
|
manager.redraw();
|
||||||
|
|
||||||
|
expect(manager).not.toContainRaw('Hello, world!');
|
||||||
|
expect(manager).not.toContainRaw('Goodbye, world!');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,46 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import GambitManager from '../../../src/common/GambitManager';
|
||||||
|
import { app } from '../../../src/forum';
|
||||||
|
|
||||||
|
const gambits = new GambitManager();
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('GambitManager', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
app.boot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('gambits are converted to filters', function () {
|
||||||
|
expect(gambits.apply('discussions', { q: 'lorem created:2023-07-07 is:hidden author:behz' })).toStrictEqual({
|
||||||
|
q: 'lorem',
|
||||||
|
created: '2023-07-07',
|
||||||
|
hidden: true,
|
||||||
|
author: 'behz',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('gambits are negated when prefixed with a dash', function () {
|
||||||
|
expect(gambits.apply('discussions', { q: 'lorem -created:2023-07-07 -is:hidden -author:behz' })).toStrictEqual({
|
||||||
|
q: 'lorem',
|
||||||
|
'-created': '2023-07-07',
|
||||||
|
'-hidden': true,
|
||||||
|
'-author': 'behz',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('gambits are only applied for the correct resource type', function () {
|
||||||
|
expect(gambits.apply('users', { q: 'lorem created:2023-07-07 is:hidden author:behz email:behz@machine.local' })).toStrictEqual({
|
||||||
|
q: 'lorem created:2023-07-07 is:hidden author:behz',
|
||||||
|
email: 'behz@machine.local',
|
||||||
|
});
|
||||||
|
expect(gambits.apply('discussions', { q: 'lorem created:2023-07-07..2023-10-18 is:hidden -author:behz email:behz@machine.local' })).toStrictEqual(
|
||||||
|
{
|
||||||
|
q: 'lorem email:behz@machine.local',
|
||||||
|
created: '2023-07-07..2023-10-18',
|
||||||
|
hidden: true,
|
||||||
|
'-author': 'behz',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,93 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import { app } from '../../../src/forum';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
import ModalManager from '../../../src/common/components/ModalManager';
|
||||||
|
import Modal from '../../../src/common/components/Modal';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('ModalManager', () => {
|
||||||
|
beforeAll(() => app.boot());
|
||||||
|
|
||||||
|
test('can show and close a modal', () => {
|
||||||
|
const manager = mq(ModalManager, { state: app.modal });
|
||||||
|
|
||||||
|
app.modal.show(MyModal);
|
||||||
|
|
||||||
|
manager.redraw();
|
||||||
|
|
||||||
|
expect(manager).toHaveElement('.ModalManager');
|
||||||
|
expect(manager).toHaveElement('.ModalManager[data-modal-number="0"]');
|
||||||
|
expect(manager).toHaveElement('.Modal');
|
||||||
|
expect(manager).toContainRaw('Hello, world!');
|
||||||
|
|
||||||
|
app.modal.close();
|
||||||
|
|
||||||
|
manager.redraw();
|
||||||
|
|
||||||
|
expect(manager).not.toHaveElement('.Modal');
|
||||||
|
expect(manager).not.toContainRaw('Hello, world!');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can stack modals', () => {
|
||||||
|
const manager = mq(ModalManager, { state: app.modal });
|
||||||
|
|
||||||
|
app.modal.show(MyModal);
|
||||||
|
app.modal.show(MySecondModal, {}, true);
|
||||||
|
|
||||||
|
manager.redraw();
|
||||||
|
|
||||||
|
expect(manager).toHaveElement('.ModalManager[data-modal-number="0"]');
|
||||||
|
expect(manager).toHaveElement('.ModalManager[data-modal-number="1"]');
|
||||||
|
expect(manager).toHaveElement('.Modal');
|
||||||
|
expect(manager).toContainRaw('Hello, world!');
|
||||||
|
expect(manager).toContainRaw('Really cool content');
|
||||||
|
|
||||||
|
app.modal.close();
|
||||||
|
|
||||||
|
manager.redraw();
|
||||||
|
|
||||||
|
expect(manager).toHaveElement('.ModalManager[data-modal-number="0"]');
|
||||||
|
expect(manager).not.toHaveElement('.ModalManager[data-modal-number="1"]');
|
||||||
|
expect(manager).toHaveElement('.Modal');
|
||||||
|
expect(manager).not.toContainRaw('Really cool content');
|
||||||
|
expect(manager).toContainRaw('Hello, world!');
|
||||||
|
|
||||||
|
app.modal.close();
|
||||||
|
|
||||||
|
manager.redraw();
|
||||||
|
|
||||||
|
expect(manager).not.toHaveElement('.ModalManager[data-modal-number="0"]');
|
||||||
|
expect(manager).not.toHaveElement('.ModalManager[data-modal-number="1"]');
|
||||||
|
expect(manager).not.toHaveElement('.Modal');
|
||||||
|
expect(manager).not.toContainRaw('Hello, world!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
class MyModal extends Modal {
|
||||||
|
className(): string {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
content() {
|
||||||
|
return 'Hello, world!';
|
||||||
|
}
|
||||||
|
|
||||||
|
title() {
|
||||||
|
return 'My Modal';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MySecondModal extends Modal {
|
||||||
|
className(): string {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
content() {
|
||||||
|
return 'Really cool content';
|
||||||
|
}
|
||||||
|
|
||||||
|
title() {
|
||||||
|
return 'My Second Modal';
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,11 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
import Alert from '../../../../src/common/components/Alert';
|
import Alert from '../../../../src/common/components/Alert';
|
||||||
import m from 'mithril';
|
import m from 'mithril';
|
||||||
import mq from 'mithril-query';
|
import mq from 'mithril-query';
|
||||||
import { jest } from '@jest/globals';
|
import { jest } from '@jest/globals';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
describe('Alert displays as expected', () => {
|
describe('Alert displays as expected', () => {
|
||||||
it('should display alert messages with an icon', () => {
|
it('should display alert messages with an icon', () => {
|
||||||
const alert = mq(m(Alert, { type: 'error' }, 'Shoot!'));
|
const alert = mq(m(Alert, { type: 'error' }, 'Shoot!'));
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import Avatar from '../../../../src/common/components/Avatar';
|
||||||
|
import m from 'mithril';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
import User from '../../../../src/common/models/User';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('Avatar displays as expected', () => {
|
||||||
|
it('renders avatar when user is deleted', () => {
|
||||||
|
const avatar = mq(m(Avatar, { user: null, className: 'test' }));
|
||||||
|
expect(avatar).toHaveElement('span.Avatar.test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders avatar when user has avatarUrl', () => {
|
||||||
|
const user = new User({
|
||||||
|
attributes: {
|
||||||
|
avatarUrl: 'http://example.com/avatar.png',
|
||||||
|
color: '#000000',
|
||||||
|
username: 'test',
|
||||||
|
displayName: 'test',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const avatar = mq(m(Avatar, { user }));
|
||||||
|
expect(avatar).toHaveElement('img.Avatar');
|
||||||
|
expect(avatar).toHaveElementAttr('img.Avatar', 'src', 'http://example.com/avatar.png');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,29 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import Badge from '../../../../src/common/components/Badge';
|
||||||
|
import m from 'mithril';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('Badge displays as expected', () => {
|
||||||
|
it('renders badge without a tooltip', () => {
|
||||||
|
const badge = mq(
|
||||||
|
m(Badge, {
|
||||||
|
icon: 'fas fa-user',
|
||||||
|
type: 'Test',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(badge).toHaveElement('.Badge.Badge--Test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders badge with a tooltip', () => {
|
||||||
|
const badge = mq(
|
||||||
|
m(Badge, {
|
||||||
|
icon: 'fas fa-user',
|
||||||
|
type: 'Test',
|
||||||
|
label: 'Tooltip',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(badge).toHaveElement('.Badge.Badge--Test');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,72 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import Button from '../../../../src/common/components/Button';
|
||||||
|
import m from 'mithril';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
import { jest } from '@jest/globals';
|
||||||
|
import { app } from '../../../../src/forum';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('Button displays as expected', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
app.boot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders button with text and icon', () => {
|
||||||
|
const button = mq(
|
||||||
|
m(
|
||||||
|
Button,
|
||||||
|
{
|
||||||
|
icon: 'fas fa-check',
|
||||||
|
'aria-label': 'Aria label',
|
||||||
|
},
|
||||||
|
'Submit'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(button).toHaveElement('button.hasIcon');
|
||||||
|
expect(button).toHaveElementAttr('button', 'aria-label', 'Aria label');
|
||||||
|
expect(button).toHaveElement('.Button-label');
|
||||||
|
expect(button).toContainRaw('Submit');
|
||||||
|
expect(button).toHaveElement('.icon.fas.fa-check');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can be disabled', () => {
|
||||||
|
const onclick = jest.fn();
|
||||||
|
const button = mq(
|
||||||
|
Button,
|
||||||
|
{
|
||||||
|
disabled: true,
|
||||||
|
onclick,
|
||||||
|
},
|
||||||
|
'Submit'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(button).toHaveElement('button.disabled');
|
||||||
|
button.click('button');
|
||||||
|
expect(onclick).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can be clicked', () => {
|
||||||
|
const onclick = jest.fn();
|
||||||
|
const button = mq(Button, { onclick });
|
||||||
|
button.click('button');
|
||||||
|
expect(onclick).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can be loading', () => {
|
||||||
|
const onclick = jest.fn();
|
||||||
|
const button = mq(
|
||||||
|
Button,
|
||||||
|
{
|
||||||
|
loading: true,
|
||||||
|
onclick,
|
||||||
|
},
|
||||||
|
'Submit'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(button).toHaveElement('.loading');
|
||||||
|
button.click('button');
|
||||||
|
expect(onclick).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,33 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import Checkbox from '../../../../src/common/components/Checkbox';
|
||||||
|
import m from 'mithril';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
import { jest } from '@jest/globals';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('Checkbox displays as expected', () => {
|
||||||
|
it('renders checkbox with text', () => {
|
||||||
|
const checkbox = mq(
|
||||||
|
m(
|
||||||
|
Checkbox,
|
||||||
|
{
|
||||||
|
state: true,
|
||||||
|
onchange: jest.fn(),
|
||||||
|
},
|
||||||
|
'Toggle This For Me'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(checkbox).toHaveElement('label.Checkbox.on');
|
||||||
|
expect(checkbox).toContainRaw('Toggle This For Me');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can be toggled', () => {
|
||||||
|
const onchange = jest.fn();
|
||||||
|
const checkbox = mq(Checkbox, { onchange, state: true });
|
||||||
|
// @ts-ignore
|
||||||
|
checkbox.trigger('input', 'change', { target: new EventTarget() });
|
||||||
|
expect(onchange).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,32 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import ColorPreviewInput from '../../../../src/common/components/ColorPreviewInput';
|
||||||
|
import m from 'mithril';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
import { jest } from '@jest/globals';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('ColorPreviewInput displays as expected', () => {
|
||||||
|
it('renders', () => {
|
||||||
|
const input = mq(m(ColorPreviewInput, { value: '#000000' }));
|
||||||
|
expect(input).toHaveElement('.FormControl');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles correct values', () => {
|
||||||
|
const onchange = jest.fn();
|
||||||
|
const input = mq(ColorPreviewInput, { value: '#000000', onchange });
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
input.trigger('input[type=color]', 'blur', { target: {} });
|
||||||
|
expect(onchange).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles wrong values', () => {
|
||||||
|
const onchange = jest.fn();
|
||||||
|
const input = mq(ColorPreviewInput, { value: '#fe', onchange });
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
input.trigger('input[type=color]', 'blur', { target: {} });
|
||||||
|
expect(onchange).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,31 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import Dropdown from '../../../../src/common/components/Dropdown';
|
||||||
|
import Button from '../../../../src/common/components/Button';
|
||||||
|
import m from 'mithril';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
import { jest } from '@jest/globals';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('Dropdown displays as expected', () => {
|
||||||
|
it('renders', () => {
|
||||||
|
const dropdown = mq(
|
||||||
|
m(
|
||||||
|
Dropdown,
|
||||||
|
{
|
||||||
|
label: 'click me!',
|
||||||
|
icon: 'fas fa-cog',
|
||||||
|
tooltip: 'tooltip!',
|
||||||
|
caretIcon: 'fas fa-caret-down',
|
||||||
|
buttonClassName: 'buttonClassName',
|
||||||
|
accessibleToggleLabel: 'toggle',
|
||||||
|
menuClassName: 'menuClassName',
|
||||||
|
onshow: jest.fn(),
|
||||||
|
onhide: jest.fn(),
|
||||||
|
},
|
||||||
|
[m(Button, { onclick: jest.fn() }, 'button 1'), m(Button, { onclick: jest.fn() }, 'button 2')]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
expect(dropdown).toContainRaw('click me!');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,24 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import FieldSet from '../../../../src/common/components/FieldSet';
|
||||||
|
import m from 'mithril';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('FieldSet displays as expected', () => {
|
||||||
|
it('renders', () => {
|
||||||
|
const input = mq(
|
||||||
|
m(
|
||||||
|
FieldSet,
|
||||||
|
{
|
||||||
|
label: 'Test FieldSet',
|
||||||
|
description: 'This is a test fieldset',
|
||||||
|
},
|
||||||
|
'child nodes'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
expect(input).toContainRaw('Test FieldSet');
|
||||||
|
expect(input).toContainRaw('This is a test fieldset');
|
||||||
|
expect(input).toContainRaw('child nodes');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,59 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import FormModal from '../../../../src/common/components/FormModal';
|
||||||
|
import ModalManagerState from '../../../../src/common/states/ModalManagerState';
|
||||||
|
import m from 'mithril';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
import { jest } from '@jest/globals';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('FormModal displays as expected', () => {
|
||||||
|
it('renders', () => {
|
||||||
|
const modal = mq(
|
||||||
|
m(TestFormModal, {
|
||||||
|
state: new ModalManagerState(),
|
||||||
|
animateShow: () => {},
|
||||||
|
animateHide: () => {},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(modal).toHaveElement('form');
|
||||||
|
expect(modal).toContainRaw('test title');
|
||||||
|
expect(modal).toContainRaw('test content');
|
||||||
|
expect(modal).toHaveElement('.TestClass');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('submits', () => {
|
||||||
|
const onsubmit = jest.fn();
|
||||||
|
const modal = mq(TestFormModal, {
|
||||||
|
state: new ModalManagerState(),
|
||||||
|
animateShow: () => {},
|
||||||
|
animateHide: () => {},
|
||||||
|
onsubmit,
|
||||||
|
});
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
modal.trigger('form', 'submit', {});
|
||||||
|
|
||||||
|
expect(onsubmit).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
class TestFormModal extends FormModal {
|
||||||
|
className(): string {
|
||||||
|
return 'TestClass';
|
||||||
|
}
|
||||||
|
|
||||||
|
content() {
|
||||||
|
return 'test content';
|
||||||
|
}
|
||||||
|
|
||||||
|
title() {
|
||||||
|
return 'test title';
|
||||||
|
}
|
||||||
|
|
||||||
|
onsubmit(e: SubmitEvent) {
|
||||||
|
// @ts-ignore
|
||||||
|
this.attrs.onsubmit?.(e);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import Icon from '../../../../src/common/components/Icon';
|
||||||
|
import m from 'mithril';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('Icon displays as expected', () => {
|
||||||
|
it('renders', () => {
|
||||||
|
const icon = mq(
|
||||||
|
m(Icon, {
|
||||||
|
name: 'fas fa-user',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(icon).toHaveElement('.fas.fa-user');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,53 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import Input from '../../../../src/common/components/Input';
|
||||||
|
import m from 'mithril';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
import { jest } from '@jest/globals';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('Input displays as expected', () => {
|
||||||
|
it('renders', () => {
|
||||||
|
const input = mq(
|
||||||
|
m(Input, {
|
||||||
|
placeholder: 'Lorem',
|
||||||
|
value: 'Ipsum',
|
||||||
|
prefixIcon: 'fas fa-user',
|
||||||
|
ariaLabel: 'Dolor',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(input).toHaveElement('input');
|
||||||
|
expect(input).toContainRaw('Ipsum');
|
||||||
|
expect(input).toHaveElementAttr('input', 'aria-label', 'Dolor');
|
||||||
|
expect(input).toHaveElementAttr('input', 'placeholder', 'Lorem');
|
||||||
|
expect(input).toHaveElement('.fas.fa-user');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can be cleared', () => {
|
||||||
|
const onchange = jest.fn();
|
||||||
|
const input = mq(Input, {
|
||||||
|
clearable: true,
|
||||||
|
clearLabel: 'Clear',
|
||||||
|
onchange,
|
||||||
|
value: 'Ipsum',
|
||||||
|
});
|
||||||
|
expect(input).toHaveElementAttr('.Input-clear', 'aria-label', 'Clear');
|
||||||
|
input.click('.Input-clear');
|
||||||
|
expect(onchange).toHaveBeenCalledWith('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can be loading', () => {
|
||||||
|
const input = mq(Input, {
|
||||||
|
loading: true,
|
||||||
|
});
|
||||||
|
expect(input).toHaveElement('.LoadingIndicator');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can be disabled', () => {
|
||||||
|
const input = mq(Input, {
|
||||||
|
disabled: true,
|
||||||
|
});
|
||||||
|
expect(input).toHaveElementAttr('input', 'disabled');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,27 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import Link from '../../../../src/common/components/Link';
|
||||||
|
import LinkButton from '../../../../src/common/components/LinkButton';
|
||||||
|
import m from 'mithril';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('Link displays as expected', () => {
|
||||||
|
it('renders as simple link', () => {
|
||||||
|
const link = mq(
|
||||||
|
m(Link, {
|
||||||
|
href: '/user',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(link).toHaveElement('a');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders as button link', () => {
|
||||||
|
const link = mq(
|
||||||
|
m(LinkButton, {
|
||||||
|
href: '/user',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(link).toHaveElement('a');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,22 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import LoadingIndicator from '../../../../src/common/components/LoadingIndicator';
|
||||||
|
import m from 'mithril';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('LoadingIndicator displays as expected', () => {
|
||||||
|
it('renders as simple link', () => {
|
||||||
|
const indicator = mq(
|
||||||
|
m(LoadingIndicator, {
|
||||||
|
display: 'block',
|
||||||
|
size: 'large',
|
||||||
|
containerClassName: 'container',
|
||||||
|
containerAttrs: { 'data-test': 'test' },
|
||||||
|
className: 'indicator',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(indicator).toHaveElementAttr('.container', 'data-test', 'test');
|
||||||
|
expect(indicator).toHaveElement('.indicator');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,76 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import Modal from '../../../../src/common/components/Modal';
|
||||||
|
import ModalManagerState from '../../../../src/common/states/ModalManagerState';
|
||||||
|
import m from 'mithril';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
import { jest } from '@jest/globals';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('Modal displays as expected', () => {
|
||||||
|
it('renders', () => {
|
||||||
|
const modal = mq(
|
||||||
|
m(TestModal, {
|
||||||
|
state: new ModalManagerState(),
|
||||||
|
animateShow: () => {},
|
||||||
|
animateHide: () => {},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(modal).toContainRaw('test title');
|
||||||
|
expect(modal).toContainRaw('test content');
|
||||||
|
expect(modal).toHaveElement('.TestClass');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can be dismissed via close button', () => {
|
||||||
|
const animateHide = jest.fn();
|
||||||
|
const modal = mq(TestModal, {
|
||||||
|
state: new ModalManagerState(),
|
||||||
|
animateShow: () => {},
|
||||||
|
animateHide,
|
||||||
|
});
|
||||||
|
|
||||||
|
modal.click('.Modal-close button');
|
||||||
|
expect(animateHide).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cannot be dismissed via close button', () => {
|
||||||
|
const modal = mq(UndismissableModal, {
|
||||||
|
state: new ModalManagerState(),
|
||||||
|
animateShow: () => {},
|
||||||
|
animateHide: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(modal).not.toHaveElement('.Modal-close button');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
class TestModal extends Modal {
|
||||||
|
className(): string {
|
||||||
|
return 'TestClass';
|
||||||
|
}
|
||||||
|
|
||||||
|
content() {
|
||||||
|
return 'test content';
|
||||||
|
}
|
||||||
|
|
||||||
|
title() {
|
||||||
|
return 'test title';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UndismissableModal extends Modal {
|
||||||
|
protected static readonly isDismissibleViaCloseButton: boolean = false;
|
||||||
|
|
||||||
|
className(): string {
|
||||||
|
return 'UndismissableModal';
|
||||||
|
}
|
||||||
|
|
||||||
|
content() {
|
||||||
|
return 'test undismissable content';
|
||||||
|
}
|
||||||
|
|
||||||
|
title() {
|
||||||
|
return 'test undismissable title';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import MultiSelect from '../../../../src/common/components/MultiSelect';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
import { jest } from '@jest/globals';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('MultiSelect displays as expected', () => {
|
||||||
|
it('works as expected', () => {
|
||||||
|
const onchange = jest.fn();
|
||||||
|
const select = mq(MultiSelect, {
|
||||||
|
options: {
|
||||||
|
a: 'Option A',
|
||||||
|
b: 'Option B',
|
||||||
|
c: {
|
||||||
|
label: 'Option C',
|
||||||
|
disabled: true,
|
||||||
|
tooltip: 'Disabled',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
value: ['b'],
|
||||||
|
wrapperAttrs: { 'data-test': 'test' },
|
||||||
|
className: 'select',
|
||||||
|
onchange,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(select).toHaveElementAttr('.Select', 'data-test', 'test');
|
||||||
|
expect(select).toContainRaw('Option A');
|
||||||
|
expect(select).toContainRaw('Option B');
|
||||||
|
expect(select).toContainRaw('Option C');
|
||||||
|
|
||||||
|
select.click('.Dropdown-item--a');
|
||||||
|
expect(onchange).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onchange).toHaveBeenCalledWith(['b', 'a']);
|
||||||
|
|
||||||
|
select.click('.Dropdown-item--b');
|
||||||
|
expect(onchange).toHaveBeenCalledTimes(2);
|
||||||
|
expect(onchange).toHaveBeenCalledWith(['a']);
|
||||||
|
|
||||||
|
select.click('.Dropdown-item--c');
|
||||||
|
expect(onchange).toHaveBeenCalledTimes(2);
|
||||||
|
expect(onchange).toHaveBeenCalledWith(['a']);
|
||||||
|
|
||||||
|
select.click('.Dropdown-item--a');
|
||||||
|
expect(onchange).toHaveBeenCalledTimes(3);
|
||||||
|
expect(onchange).toHaveBeenCalledWith([]);
|
||||||
|
|
||||||
|
select.click('.Dropdown-item--b');
|
||||||
|
expect(onchange).toHaveBeenCalledTimes(4);
|
||||||
|
expect(onchange).toHaveBeenCalledWith(['b']);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,22 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import Navigation from '../../../../src/common/components/Navigation';
|
||||||
|
import { app } from '../../../../src/forum';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('Navigation', () => {
|
||||||
|
beforeAll(() => app.boot());
|
||||||
|
|
||||||
|
test('renders as normal nav', () => {
|
||||||
|
const nav = mq(Navigation);
|
||||||
|
|
||||||
|
expect(nav).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders as drawer', () => {
|
||||||
|
const nav = mq(Navigation, { drawer: true });
|
||||||
|
|
||||||
|
expect(nav).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,39 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import Select from '../../../../src/common/components/Select';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
import { jest } from '@jest/globals';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('Select displays as expected', () => {
|
||||||
|
it('works as expected', () => {
|
||||||
|
const onchange = jest.fn();
|
||||||
|
const select = mq(Select, {
|
||||||
|
options: {
|
||||||
|
a: 'Option A',
|
||||||
|
b: 'Option B',
|
||||||
|
c: {
|
||||||
|
label: 'Option C',
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
value: null,
|
||||||
|
wrapperAttrs: { 'data-test': 'test' },
|
||||||
|
className: 'select',
|
||||||
|
onchange,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(select).toHaveElementAttr('.Select', 'data-test', 'test');
|
||||||
|
expect(select).toContainRaw('Option A');
|
||||||
|
expect(select).toContainRaw('Option B');
|
||||||
|
expect(select).toContainRaw('Option C');
|
||||||
|
|
||||||
|
select.setValue('select', 'a');
|
||||||
|
expect(onchange).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onchange).toHaveBeenCalledWith('a');
|
||||||
|
|
||||||
|
select.setValue('select', 'b');
|
||||||
|
expect(onchange).toHaveBeenCalledTimes(2);
|
||||||
|
expect(onchange).toHaveBeenCalledWith('b');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,54 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import SelectDropdown from '../../../../src/common/components/SelectDropdown';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
import m from 'mithril';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('SelectDropdown displays as expected', () => {
|
||||||
|
it('works as expected', () => {
|
||||||
|
const buttons = [
|
||||||
|
m('button', { className: 'button-1' }, 'Option A'),
|
||||||
|
m('button', { className: 'button-2' }, 'Option B'),
|
||||||
|
m('button', { className: 'button-3' }, 'Option C'),
|
||||||
|
];
|
||||||
|
|
||||||
|
const select = mq(
|
||||||
|
m(
|
||||||
|
SelectDropdown,
|
||||||
|
{
|
||||||
|
label: 'Select the option',
|
||||||
|
defaultLabel: 'Select an option',
|
||||||
|
},
|
||||||
|
buttons
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(select).toContainRaw('Select an option');
|
||||||
|
expect(select).toContainRaw('Option A');
|
||||||
|
expect(select).toContainRaw('Option B');
|
||||||
|
expect(select).toContainRaw('Option C');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses active button as label', () => {
|
||||||
|
const buttons = [
|
||||||
|
m('button', { className: 'button-1', active: false }, 'Option A'),
|
||||||
|
m('button', { className: 'button-2', active: true }, 'Option B'),
|
||||||
|
m('button', { className: 'button-3', active: false }, 'Option C'),
|
||||||
|
];
|
||||||
|
|
||||||
|
const select = mq(
|
||||||
|
m(
|
||||||
|
SelectDropdown,
|
||||||
|
{
|
||||||
|
label: 'Select the option',
|
||||||
|
defaultLabel: 'Select an option',
|
||||||
|
},
|
||||||
|
buttons
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(select).toContainRaw('Option B');
|
||||||
|
expect(select).not.toContainRaw('Select an option');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,39 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import SplitDropdown from '../../../../src/common/components/SplitDropdown';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
import m from 'mithril';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('SplitDropdown displays as expected', () => {
|
||||||
|
it('works as expected', () => {
|
||||||
|
const buttons = [
|
||||||
|
m('button', { className: 'button-1' }, 'Option A'),
|
||||||
|
m('button', { className: 'button-2' }, 'Option B'),
|
||||||
|
m('button', { className: 'button-3' }, 'Option C'),
|
||||||
|
];
|
||||||
|
|
||||||
|
const select = mq(
|
||||||
|
m(
|
||||||
|
SplitDropdown,
|
||||||
|
{
|
||||||
|
label: 'Select the option',
|
||||||
|
},
|
||||||
|
buttons
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(select).not.toContainRaw('Select an option');
|
||||||
|
expect(select).toContainRaw('Option A');
|
||||||
|
expect(select).toContainRaw('Option B');
|
||||||
|
expect(select).toContainRaw('Option C');
|
||||||
|
|
||||||
|
// First button is displayed as its own button separate from the dropdown
|
||||||
|
expect(select).toHaveElement('.SplitDropdown-button.button-1');
|
||||||
|
expect(select).toHaveElement('li .button-1');
|
||||||
|
expect(select).not.toHaveElement('.SplitDropdown-button.button-2');
|
||||||
|
expect(select).toHaveElement('li .button-2');
|
||||||
|
expect(select).not.toHaveElement('.SplitDropdown-button.button-3');
|
||||||
|
expect(select).toHaveElement('li .button-3');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,102 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import Model from '../../../../src/common/extenders/Model';
|
||||||
|
import Discussion from '../../../../src/common/models/Discussion';
|
||||||
|
import User from '../../../../src/common/models/User';
|
||||||
|
import app from '@flarum/core/src/forum/app';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('Model extender', () => {
|
||||||
|
const discussion = new Discussion({
|
||||||
|
id: '1',
|
||||||
|
type: 'discussions',
|
||||||
|
attributes: {
|
||||||
|
title: 'Discussion title',
|
||||||
|
keanu: 'Reeves',
|
||||||
|
},
|
||||||
|
relationships: {
|
||||||
|
posts: {
|
||||||
|
data: [
|
||||||
|
{ id: '1', type: 'posts' },
|
||||||
|
{ id: '2', type: 'posts' },
|
||||||
|
{ id: '3', type: 'posts' },
|
||||||
|
{ id: '4', type: 'posts' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
idrisElba: {
|
||||||
|
data: { id: '1', type: 'users' },
|
||||||
|
},
|
||||||
|
people: {
|
||||||
|
data: [{ id: '1', type: 'users' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test('new attribute does not work if not defined', () => {
|
||||||
|
app.boot();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
expect(() => discussion.keanu()).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('added route works', () => {
|
||||||
|
app.bootExtensions({
|
||||||
|
test: {
|
||||||
|
extend: [new Model(Discussion).attribute<string>('keanu')],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
app.boot();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
expect(() => discussion.keanu()).not.toThrow();
|
||||||
|
// @ts-ignore
|
||||||
|
expect(discussion.keanu()).toBe('Reeves');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('to one relationship does not work if not defined', () => {
|
||||||
|
app.boot();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
expect(() => discussion.idrisElba()).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('added to one relationship works', () => {
|
||||||
|
app.bootExtensions({
|
||||||
|
test: {
|
||||||
|
extend: [new Model(Discussion).hasOne<User>('idrisElba')],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
app.boot();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
expect(() => discussion.idrisElba()).not.toThrow();
|
||||||
|
// @ts-ignore
|
||||||
|
expect(discussion.idrisElba()).toBeInstanceOf(User);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('to many relationship does not work if not defined', () => {
|
||||||
|
app.boot();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
expect(() => discussion.people()).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('added to many relationship works', () => {
|
||||||
|
app.bootExtensions({
|
||||||
|
test: {
|
||||||
|
extend: [new Model(Discussion).hasMany<User>('people')],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
app.boot();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
expect(() => discussion.people()).not.toThrow();
|
||||||
|
// @ts-ignore
|
||||||
|
expect(discussion.people()).toBeInstanceOf(Array);
|
||||||
|
// @ts-ignore
|
||||||
|
expect(discussion.people()[0]).toBeInstanceOf(User);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,100 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import Extend from '../../../../src/common/extenders';
|
||||||
|
import NotificationType from '../../../../src/forum/components/NotificationType';
|
||||||
|
import NotificationComponent from '../../../../src/forum/components/Notification';
|
||||||
|
import Notification from '../../../../src/common/models/Notification';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
import app from '@flarum/core/src/forum/app';
|
||||||
|
import Mithril from 'mithril';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('Notification extender', () => {
|
||||||
|
app.store.pushPayload({
|
||||||
|
data: {
|
||||||
|
id: '1',
|
||||||
|
type: 'discussions',
|
||||||
|
attributes: {
|
||||||
|
title: 'Discussion title',
|
||||||
|
},
|
||||||
|
relationships: {
|
||||||
|
posts: {
|
||||||
|
data: [
|
||||||
|
{ id: '1', type: 'posts' },
|
||||||
|
{ id: '2', type: 'posts' },
|
||||||
|
{ id: '3', type: 'posts' },
|
||||||
|
{ id: '4', type: 'posts' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const keanuNotification = new Notification({
|
||||||
|
id: '1',
|
||||||
|
type: 'notifications',
|
||||||
|
attributes: { contentType: 'keanu', canEdit: false, createdAt: new Date() },
|
||||||
|
relationships: { subject: { data: { type: 'discussions', id: '1' } } },
|
||||||
|
});
|
||||||
|
|
||||||
|
const normalNotification = new Notification({
|
||||||
|
id: '2',
|
||||||
|
type: 'notifications',
|
||||||
|
attributes: { contentType: 'discussionRenamed', canEdit: false, createdAt: new Date(), content: { postNumber: 1 } },
|
||||||
|
relationships: { subject: { data: { type: 'discussions', id: '1' } } },
|
||||||
|
});
|
||||||
|
|
||||||
|
it('existing notifications work by default', () => {
|
||||||
|
app.boot();
|
||||||
|
|
||||||
|
const notificationComponent = mq(NotificationType, {
|
||||||
|
notification: normalNotification,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(notificationComponent).toContainRaw('changed the title');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not work before registering the notification type', () => {
|
||||||
|
app.boot();
|
||||||
|
|
||||||
|
const notificationComponent = mq(NotificationType, {
|
||||||
|
notification: keanuNotification,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(notificationComponent).not.toContainRaw('Keanu Reeves is awesome');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works after registering the notification type', () => {
|
||||||
|
app.bootExtensions({
|
||||||
|
test: {
|
||||||
|
extend: [new Extend.Notification().add('keanu', KeanuNotification)],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
app.boot();
|
||||||
|
|
||||||
|
const notificationComponent = mq(NotificationType, {
|
||||||
|
notification: keanuNotification,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(notificationComponent).toContainRaw('Keanu Reeves is awesome');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
class KeanuNotification extends NotificationComponent {
|
||||||
|
icon() {
|
||||||
|
return 'fas fa-keanu';
|
||||||
|
}
|
||||||
|
|
||||||
|
content(): Mithril.Children {
|
||||||
|
return 'Keanu Reeves is awesome';
|
||||||
|
}
|
||||||
|
|
||||||
|
excerpt(): Mithril.Children {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
href(): string {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import PostTypes from '../../../../src/common/extenders/PostTypes';
|
||||||
|
import PostType from '../../../../src/forum/components/PostType';
|
||||||
|
import EventPost from '../../../../src/forum/components/EventPost';
|
||||||
|
import Post from '../../../../src/common/models/Post';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
import app from '@flarum/core/src/forum/app';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('PostTypes extender', () => {
|
||||||
|
app.store.pushPayload({
|
||||||
|
data: {
|
||||||
|
id: '1',
|
||||||
|
type: 'discussions',
|
||||||
|
attributes: {
|
||||||
|
title: 'Discussion title',
|
||||||
|
},
|
||||||
|
relationships: {
|
||||||
|
posts: {
|
||||||
|
data: [
|
||||||
|
{ id: '1', type: 'posts' },
|
||||||
|
{ id: '2', type: 'posts' },
|
||||||
|
{ id: '3', type: 'posts' },
|
||||||
|
{ id: '4', type: 'posts' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const keanuPost = new Post({
|
||||||
|
id: '1',
|
||||||
|
type: 'posts',
|
||||||
|
attributes: { contentType: 'keanu', canEdit: false, createdAt: new Date(), contentHtml: '<strong>Hi</strong>' },
|
||||||
|
relationships: { discussion: { data: { type: 'discussions', id: '1' } } },
|
||||||
|
});
|
||||||
|
|
||||||
|
const commentPost = new Post({
|
||||||
|
id: '2',
|
||||||
|
type: 'posts',
|
||||||
|
attributes: { contentType: 'comment', canEdit: false, createdAt: new Date(), contentHtml: '<strong>Bye</strong>' },
|
||||||
|
relationships: { discussion: { data: { type: 'discussions', id: '1' } } },
|
||||||
|
});
|
||||||
|
|
||||||
|
it('comment posts work by default', () => {
|
||||||
|
app.boot();
|
||||||
|
|
||||||
|
const postComponent = mq(PostType, {
|
||||||
|
post: commentPost,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(postComponent).toContainRaw('Bye');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not work before registering the post type', () => {
|
||||||
|
app.boot();
|
||||||
|
|
||||||
|
const postComponent = mq(PostType, {
|
||||||
|
post: keanuPost,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(postComponent).not.toContainRaw('Hi');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works after registering the post type', () => {
|
||||||
|
app.bootExtensions({
|
||||||
|
test: {
|
||||||
|
extend: [new PostTypes().add('keanu', KeanuPost)],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
app.boot();
|
||||||
|
|
||||||
|
const postComponent = mq(PostType, {
|
||||||
|
post: keanuPost,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(postComponent).toContainRaw('Hi');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
class KeanuPost extends EventPost {
|
||||||
|
icon() {
|
||||||
|
return 'fas fa-keanu';
|
||||||
|
}
|
||||||
|
|
||||||
|
description() {
|
||||||
|
return this.attrs.post.contentHtml();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import Routes from '../../../../src/common/extenders/Routes';
|
||||||
|
import app from '@flarum/core/src/forum/app';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('Routes extender', () => {
|
||||||
|
test('non added route does not work', () => {
|
||||||
|
app.boot();
|
||||||
|
|
||||||
|
expect(() => app.route('nonexistent')).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('added route works', () => {
|
||||||
|
app.bootExtensions({
|
||||||
|
test: {
|
||||||
|
extend: [new Routes().add('nonexistent', '/nonexistent', null)],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
app.boot();
|
||||||
|
|
||||||
|
expect(() => app.route('nonexistent')).not.toThrow();
|
||||||
|
expect(app.route('nonexistent')).toBe('/nonexistent');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('added route helper works', () => {
|
||||||
|
app.bootExtensions({
|
||||||
|
test: {
|
||||||
|
extend: [new Routes().helper('nonexistent', () => '/nonexistent')],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
app.boot();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
expect(() => app.route.nonexistent()).not.toThrow();
|
||||||
|
// @ts-ignore
|
||||||
|
expect(app.route.nonexistent()).toBe('/nonexistent');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,45 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import Search from '../../../../src/common/extenders/Search';
|
||||||
|
import { KeyValueGambit } from '../../../../src/common/query/IGambit';
|
||||||
|
import { app } from '../../../../src/forum';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('Search extender', () => {
|
||||||
|
it('gambit does not work before registering it', () => {
|
||||||
|
app.boot();
|
||||||
|
|
||||||
|
expect(app.search.gambits.apply('discussions', { q: 'lorem keanu:reeves' })).toStrictEqual({
|
||||||
|
q: 'lorem keanu:reeves',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works after registering it', () => {
|
||||||
|
app.bootExtensions({
|
||||||
|
test: {
|
||||||
|
extend: [new Search().gambit('discussions', KeanuGambit)],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
app.boot();
|
||||||
|
|
||||||
|
expect(app.search.gambits.apply('discussions', { q: 'lorem keanu:reeves' })).toStrictEqual({
|
||||||
|
q: 'lorem',
|
||||||
|
keanu: 'reeves',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
class KeanuGambit extends KeyValueGambit {
|
||||||
|
filterKey(): string {
|
||||||
|
return 'keanu';
|
||||||
|
}
|
||||||
|
|
||||||
|
hint(): string {
|
||||||
|
return 'Keanu is breathtaking';
|
||||||
|
}
|
||||||
|
|
||||||
|
key(): string {
|
||||||
|
return 'keanu';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import Extend from '../../../../src/common/extenders';
|
||||||
|
import Discussion from '../../../../src/common/models/Discussion';
|
||||||
|
import Model from '../../../../src/common/Model';
|
||||||
|
import app from '@flarum/core/src/forum/app';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('Store extender', () => {
|
||||||
|
const discussion = new Discussion({
|
||||||
|
id: '1',
|
||||||
|
type: 'discussions',
|
||||||
|
attributes: {
|
||||||
|
title: 'Discussion title',
|
||||||
|
},
|
||||||
|
relationships: {
|
||||||
|
posts: {
|
||||||
|
data: [
|
||||||
|
{ id: '1', type: 'posts' },
|
||||||
|
{ id: '2', type: 'posts' },
|
||||||
|
{ id: '3', type: 'posts' },
|
||||||
|
{ id: '4', type: 'posts' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
potato: {
|
||||||
|
data: { id: '1', type: 'potatoes' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const pushPotato = () =>
|
||||||
|
app.store.pushPayload({
|
||||||
|
data: {
|
||||||
|
type: 'potatoes',
|
||||||
|
id: '1',
|
||||||
|
attributes: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test('new model type does not work if not defined', () => {
|
||||||
|
app.boot();
|
||||||
|
|
||||||
|
expect(pushPotato).toThrow();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
expect(() => discussion.potato()).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('added route works', () => {
|
||||||
|
app.bootExtensions({
|
||||||
|
test: {
|
||||||
|
extend: [new Extend.Store().add('potatoes', Potato), new Extend.Model(Discussion).hasOne<Potato>('potato')],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
pushPotato();
|
||||||
|
|
||||||
|
app.boot();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
expect(() => discussion.potato()).not.toThrow();
|
||||||
|
// @ts-ignore
|
||||||
|
expect(discussion.potato()).toBeInstanceOf(Potato);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
class Potato extends Model {}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import HeaderSecondary from '../../../../src/forum/components/HeaderSecondary';
|
||||||
|
import { app } from '../../../../src/forum';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('HeaderSecondary', () => {
|
||||||
|
beforeAll(() => app.boot());
|
||||||
|
|
||||||
|
test('renders', () => {
|
||||||
|
const header = mq(HeaderSecondary);
|
||||||
|
|
||||||
|
expect(header).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,100 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import IndexPage from '../../../../src/forum/components/IndexPage';
|
||||||
|
import { app } from '../../../../src/forum';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
import Discussion from '../../../../src/common/models/Discussion';
|
||||||
|
import { makeDiscussion } from '../../../factory';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('IndexPage', () => {
|
||||||
|
// mock app.store.find
|
||||||
|
// @ts-ignore
|
||||||
|
app.store.find = function () {
|
||||||
|
return Promise.resolve([
|
||||||
|
new Discussion(
|
||||||
|
makeDiscussion({
|
||||||
|
id: '1',
|
||||||
|
attributes: {
|
||||||
|
title: 'Discussion 1',
|
||||||
|
slug: 'discussion-1',
|
||||||
|
},
|
||||||
|
relationships: {
|
||||||
|
firstPost: {
|
||||||
|
data: { type: 'posts', id: '2' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
),
|
||||||
|
new Discussion(
|
||||||
|
makeDiscussion({
|
||||||
|
id: '2',
|
||||||
|
attributes: {
|
||||||
|
title: 'Discussion 2',
|
||||||
|
slug: 'discussion-2',
|
||||||
|
},
|
||||||
|
relationships: {
|
||||||
|
firstPost: {
|
||||||
|
data: { type: 'posts', id: '2' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
app.boot();
|
||||||
|
app.store.pushPayload({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
type: 'posts',
|
||||||
|
id: '1',
|
||||||
|
attributes: {
|
||||||
|
content: 'Post 1',
|
||||||
|
number: 1,
|
||||||
|
},
|
||||||
|
relationships: {
|
||||||
|
discussion: {
|
||||||
|
data: { type: 'discussions', id: '1' },
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
data: { type: 'users', id: '1' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'posts',
|
||||||
|
id: '2',
|
||||||
|
attributes: {
|
||||||
|
content: 'Post 2',
|
||||||
|
number: 1,
|
||||||
|
},
|
||||||
|
relationships: {
|
||||||
|
discussion: {
|
||||||
|
data: { type: 'discussions', id: '2' },
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
data: { type: 'users', id: '1' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders', () => {
|
||||||
|
const page = mq(IndexPage, {});
|
||||||
|
|
||||||
|
// wait a tick for the store.find promise to resolve
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, 0)).then(() => {
|
||||||
|
page.redraw();
|
||||||
|
|
||||||
|
expect(page).toHaveElement('.IndexPage');
|
||||||
|
expect(page).toHaveElement('.DiscussionList');
|
||||||
|
expect(page).toHaveElement('.DiscussionListItem');
|
||||||
|
expect(page).toContainRaw('Discussion 1');
|
||||||
|
expect(page).toContainRaw('Discussion 2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,108 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
import { app } from '../../../../src/forum';
|
||||||
|
import ModalManager from '../../../../src/common/components/ModalManager';
|
||||||
|
import DiscussionsSearchSource from '../../../../src/forum/components/DiscussionsSearchSource';
|
||||||
|
import ChangeEmailModal from '../../../../src/forum/components/ChangeEmailModal';
|
||||||
|
import ChangePasswordModal from '../../../../src/forum/components/ChangePasswordModal';
|
||||||
|
import ForgotPasswordModal from '../../../../src/forum/components/ForgotPasswordModal';
|
||||||
|
import LogInModal from '../../../../src/forum/components/LogInModal';
|
||||||
|
import NewAccessTokenModal from '../../../../src/forum/components/NewAccessTokenModal';
|
||||||
|
import RenameDiscussionModal from '../../../../src/forum/components/RenameDiscussionModal';
|
||||||
|
import SearchModal from '../../../../src/common/components/SearchModal';
|
||||||
|
import SignUpModal from '../../../../src/forum/components/SignUpModal';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('Modals', () => {
|
||||||
|
beforeAll(() => app.boot());
|
||||||
|
beforeEach(() => app.modal.close());
|
||||||
|
|
||||||
|
test('ChangeEmailModal renders', () => {
|
||||||
|
const manager = mq(ModalManager, { state: app.modal });
|
||||||
|
|
||||||
|
app.modal.show(ChangeEmailModal);
|
||||||
|
|
||||||
|
manager.redraw();
|
||||||
|
|
||||||
|
expect(app.modal.modalList.length).toEqual(1);
|
||||||
|
expect(manager).toHaveElement('.ModalManager');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ChangePasswordModal renders', () => {
|
||||||
|
const manager = mq(ModalManager, { state: app.modal });
|
||||||
|
|
||||||
|
app.modal.show(ChangePasswordModal);
|
||||||
|
|
||||||
|
manager.redraw();
|
||||||
|
|
||||||
|
expect(app.modal.modalList.length).toEqual(1);
|
||||||
|
expect(manager).toHaveElement('.ModalManager');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ForgotPasswordModal renders', () => {
|
||||||
|
const manager = mq(ModalManager, { state: app.modal });
|
||||||
|
|
||||||
|
app.modal.show(ForgotPasswordModal);
|
||||||
|
|
||||||
|
manager.redraw();
|
||||||
|
|
||||||
|
expect(app.modal.modalList.length).toEqual(1);
|
||||||
|
expect(manager).toHaveElement('.ModalManager');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('LogInModal renders', () => {
|
||||||
|
const manager = mq(ModalManager, { state: app.modal });
|
||||||
|
|
||||||
|
app.modal.show(LogInModal);
|
||||||
|
|
||||||
|
manager.redraw();
|
||||||
|
|
||||||
|
expect(app.modal.modalList.length).toEqual(1);
|
||||||
|
expect(manager).toHaveElement('.ModalManager');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('NewAccessTokenModal renders', () => {
|
||||||
|
const manager = mq(ModalManager, { state: app.modal });
|
||||||
|
|
||||||
|
app.modal.show(NewAccessTokenModal);
|
||||||
|
|
||||||
|
manager.redraw();
|
||||||
|
|
||||||
|
expect(app.modal.modalList.length).toEqual(1);
|
||||||
|
expect(manager).toHaveElement('.ModalManager');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('RenameDiscussionModal renders', () => {
|
||||||
|
const manager = mq(ModalManager, { state: app.modal });
|
||||||
|
|
||||||
|
app.modal.show(RenameDiscussionModal);
|
||||||
|
|
||||||
|
manager.redraw();
|
||||||
|
|
||||||
|
expect(app.modal.modalList.length).toEqual(1);
|
||||||
|
expect(manager).toHaveElement('.ModalManager');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SearchModal renders', () => {
|
||||||
|
const manager = mq(ModalManager, { state: app.modal });
|
||||||
|
|
||||||
|
app.modal.show(SearchModal, { searchState: app.search.state, sources: [new DiscussionsSearchSource()] });
|
||||||
|
|
||||||
|
manager.redraw();
|
||||||
|
|
||||||
|
expect(app.modal.modalList.length).toEqual(1);
|
||||||
|
expect(manager).toHaveElement('.ModalManager');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SignUpModal renders', () => {
|
||||||
|
const manager = mq(ModalManager, { state: app.modal });
|
||||||
|
|
||||||
|
app.modal.show(SignUpModal);
|
||||||
|
|
||||||
|
manager.redraw();
|
||||||
|
|
||||||
|
expect(app.modal.modalList.length).toEqual(1);
|
||||||
|
expect(manager).toHaveElement('.ModalManager');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,90 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
|
import PostStream from '../../../../src/forum/components/PostStream';
|
||||||
|
import PostStreamState from '../../../../src/forum/states/PostStreamState';
|
||||||
|
import Discussion from '../../../../src/common/models/Discussion';
|
||||||
|
import app from '../../../../src/forum/app';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
|
describe('PostStream component', () => {
|
||||||
|
app.store.pushPayload({
|
||||||
|
data: {
|
||||||
|
id: '1',
|
||||||
|
type: 'discussions',
|
||||||
|
attributes: {
|
||||||
|
title: 'Discussion title',
|
||||||
|
},
|
||||||
|
relationships: {
|
||||||
|
posts: {
|
||||||
|
data: [
|
||||||
|
{ id: '1', type: 'posts' },
|
||||||
|
{ id: '2', type: 'posts' },
|
||||||
|
{ id: '3', type: 'posts' },
|
||||||
|
{ id: '4', type: 'posts' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
app.store.pushPayload({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
type: 'posts',
|
||||||
|
attributes: { contentType: 'comment', canEdit: false, createdAt: new Date(), contentHtml: '<strong>Hi</strong>' },
|
||||||
|
relationships: { discussion: { data: { type: 'discussions', id: '1' } } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
type: 'posts',
|
||||||
|
attributes: {
|
||||||
|
contentType: 'comment',
|
||||||
|
canEdit: false,
|
||||||
|
createdAt: new Date(),
|
||||||
|
contentHtml: '<strong>Bye</strong>',
|
||||||
|
},
|
||||||
|
relationships: { discussion: { data: { type: 'discussions', id: '1' } } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
type: 'posts',
|
||||||
|
attributes: {
|
||||||
|
contentType: 'comment',
|
||||||
|
canEdit: false,
|
||||||
|
createdAt: new Date(),
|
||||||
|
contentHtml: '<strong>Hi again</strong>',
|
||||||
|
},
|
||||||
|
relationships: { discussion: { data: { type: 'discussions', id: '1' } } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
type: 'posts',
|
||||||
|
attributes: {
|
||||||
|
contentType: 'comment',
|
||||||
|
canEdit: false,
|
||||||
|
createdAt: new Date(),
|
||||||
|
contentHtml: '<strong>Bye again</strong>',
|
||||||
|
},
|
||||||
|
relationships: { discussion: { data: { type: 'discussions', id: '1' } } },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const discussion = app.store.getById<Discussion>('discussions', '1');
|
||||||
|
|
||||||
|
it('renders correctly', () => {
|
||||||
|
app.boot();
|
||||||
|
|
||||||
|
const postStream = mq(PostStream, {
|
||||||
|
stream: new PostStreamState(discussion, app.store.all('posts')),
|
||||||
|
discussion,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(postStream).toContainRaw('Hi');
|
||||||
|
expect(postStream).toContainRaw('Bye');
|
||||||
|
expect(postStream).toContainRaw('Hi again');
|
||||||
|
expect(postStream).toContainRaw('Bye again');
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,34 +0,0 @@
|
||||||
import GambitManager from '../../../src/common/GambitManager';
|
|
||||||
|
|
||||||
const gambits = new GambitManager();
|
|
||||||
|
|
||||||
test('gambits are converted to filters', function () {
|
|
||||||
expect(gambits.apply('discussions', { q: 'lorem created:2023-07-07 is:hidden author:behz' })).toStrictEqual({
|
|
||||||
q: 'lorem',
|
|
||||||
created: '2023-07-07',
|
|
||||||
hidden: true,
|
|
||||||
author: 'behz',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('gambits are negated when prefixed with a dash', function () {
|
|
||||||
expect(gambits.apply('discussions', { q: 'lorem -created:2023-07-07 -is:hidden -author:behz' })).toStrictEqual({
|
|
||||||
q: 'lorem',
|
|
||||||
'-created': '2023-07-07',
|
|
||||||
'-hidden': true,
|
|
||||||
'-author': 'behz',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('gambits are only applied for the correct resource type', function () {
|
|
||||||
expect(gambits.apply('users', { q: 'lorem created:2023-07-07 is:hidden author:behz email:behz@machine.local' })).toStrictEqual({
|
|
||||||
q: 'lorem created:2023-07-07 is:hidden author:behz',
|
|
||||||
email: 'behz@machine.local',
|
|
||||||
});
|
|
||||||
expect(gambits.apply('discussions', { q: 'lorem created:2023-07-07..2023-10-18 is:hidden -author:behz email:behz@machine.local' })).toStrictEqual({
|
|
||||||
q: 'lorem email:behz@machine.local',
|
|
||||||
created: '2023-07-07..2023-10-18',
|
|
||||||
hidden: true,
|
|
||||||
'-author': 'behz',
|
|
||||||
});
|
|
||||||
});
|
|
129
framework/core/js/tests/unit/common/extend.test.ts
Normal file
129
framework/core/js/tests/unit/common/extend.test.ts
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
import { extend, override } from '../../../src/common/extend';
|
||||||
|
import Component from '../../../src/common/Component';
|
||||||
|
import Mithril from 'mithril';
|
||||||
|
import m from 'mithril';
|
||||||
|
import mq from 'mithril-query';
|
||||||
|
|
||||||
|
describe('extend', () => {
|
||||||
|
test('can extend component methods', () => {
|
||||||
|
extend(Acme.prototype, 'view', function (original) {
|
||||||
|
original.children.push(m('p', 'This is a test extension.'));
|
||||||
|
});
|
||||||
|
|
||||||
|
const acme = mq(Acme);
|
||||||
|
|
||||||
|
expect(acme).toContainRaw('This is a test extension.');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can extend multiple methods at once', () => {
|
||||||
|
let counter = 0;
|
||||||
|
extend(Acme.prototype, ['items', 'otherItems'], function (original) {
|
||||||
|
const dataCount = counter++;
|
||||||
|
original.children.push(m('li', { 'data-count': dataCount }, 'Breaking News!'));
|
||||||
|
});
|
||||||
|
|
||||||
|
const acme = mq(Acme);
|
||||||
|
|
||||||
|
expect(acme).toContainRaw('Breaking News!');
|
||||||
|
expect(acme).toHaveElement('li[data-count="0"]');
|
||||||
|
expect(acme).toHaveElement('li[data-count="1"]');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can extend lazy loaded components', () => {
|
||||||
|
extend('flarum/common/components/Lazy', 'view', function (original) {
|
||||||
|
original.children.push(m('div', 'Loaded the lazy component.'));
|
||||||
|
});
|
||||||
|
|
||||||
|
const lazy = mq(Lazy);
|
||||||
|
|
||||||
|
expect(lazy).toContainRaw('Lazy loaded component.');
|
||||||
|
expect(lazy).not.toContainRaw('Loaded the lazy component.');
|
||||||
|
|
||||||
|
// Emulate the lazy loading of the component.
|
||||||
|
// @ts-ignore
|
||||||
|
flarum.reg.add('core', 'common/components/Lazy', Lazy);
|
||||||
|
|
||||||
|
const lazy2 = mq(Lazy);
|
||||||
|
|
||||||
|
expect(lazy2).toContainRaw('Lazy loaded component.');
|
||||||
|
expect(lazy2).toContainRaw('Loaded the lazy component.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('override', () => {
|
||||||
|
test('can override component methods', () => {
|
||||||
|
override(Acme.prototype, 'items', function (original) {
|
||||||
|
return m('ul', [m('li', 'ItemOverride 1'), m('li', 'ItemOverride 2'), m('li', 'ItemOverride 3')]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const acme = mq(Acme);
|
||||||
|
|
||||||
|
expect(acme).toContainRaw('ItemOverride 1');
|
||||||
|
expect(acme).toContainRaw('ItemOverride 2');
|
||||||
|
expect(acme).toContainRaw('ItemOverride 3');
|
||||||
|
|
||||||
|
expect(acme).not.toContainRaw('Item 1');
|
||||||
|
expect(acme).not.toContainRaw('Item 2');
|
||||||
|
expect(acme).not.toContainRaw('Item 3');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can override multiple methods at once', () => {
|
||||||
|
override(Acme.prototype, ['items', 'otherItems'], function (original) {
|
||||||
|
return m('ul', [m('li', 'ItemOverride 1'), m('li', 'ItemOverride 2'), m('li', 'ItemOverride 3')]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const acme = mq(Acme);
|
||||||
|
|
||||||
|
expect(acme).toContainRaw('ItemOverride 1');
|
||||||
|
expect(acme).toContainRaw('ItemOverride 2');
|
||||||
|
expect(acme).toContainRaw('ItemOverride 3');
|
||||||
|
|
||||||
|
expect(acme).not.toContainRaw('Item 1');
|
||||||
|
expect(acme).not.toContainRaw('Item 2');
|
||||||
|
expect(acme).not.toContainRaw('Item 3');
|
||||||
|
|
||||||
|
expect(acme).not.toContainRaw('ItemOther 1');
|
||||||
|
expect(acme).not.toContainRaw('ItemOther 2');
|
||||||
|
expect(acme).not.toContainRaw('ItemOther 3');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can override lazy loaded components', () => {
|
||||||
|
override('flarum/common/components/Lazy', 'view', function (original) {
|
||||||
|
return m('div', 'Overridden lazy component.');
|
||||||
|
});
|
||||||
|
|
||||||
|
const lazy = mq(Lazy);
|
||||||
|
|
||||||
|
expect(lazy).toContainRaw('Lazy loaded component.');
|
||||||
|
expect(lazy).not.toContainRaw('Overridden lazy component.');
|
||||||
|
|
||||||
|
// Emulate the lazy loading of the component.
|
||||||
|
// @ts-ignore
|
||||||
|
flarum.reg.add('core', 'common/components/Lazy', Lazy);
|
||||||
|
|
||||||
|
const lazy2 = mq(Lazy);
|
||||||
|
|
||||||
|
expect(lazy2).not.toContainRaw('Lazy loaded component.');
|
||||||
|
expect(lazy2).toContainRaw('Overridden lazy component.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
class Acme extends Component {
|
||||||
|
view(): Mithril.Children {
|
||||||
|
return m('div', { class: 'Acme' }, [m('h1', m('div', this.items())), m('p', 'This is a test component.'), m('div', this.otherItems())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
items() {
|
||||||
|
return m('ul', [m('li', 'Item 1'), m('li', 'Item 2'), m('li', 'Item 3')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
otherItems() {
|
||||||
|
return m('ul', [m('li', 'ItemOther 1'), m('li', 'ItemOther 2'), m('li', 'ItemOther 3')]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Lazy extends Component {
|
||||||
|
view(): Mithril.Children {
|
||||||
|
return m('div', 'Lazy loaded component.');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
import highlight from '../../../../src/common/helpers/highlight';
|
||||||
|
|
||||||
|
describe('highlight', () => {
|
||||||
|
it('should return the string if no phrase or length is given', () => {
|
||||||
|
const string = 'Hello, world!';
|
||||||
|
expect(highlight(string)).toBe(string);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should highlight a phrase in a string', () => {
|
||||||
|
const string = 'Hello, world!';
|
||||||
|
const phrase = 'world';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
expect(highlight(string, phrase).children).toBe('Hello, <mark>world</mark>!');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should highlight a phrase in a string case-insensitively', () => {
|
||||||
|
const string = 'Hello, world!';
|
||||||
|
const phrase = 'WORLD';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
expect(highlight(string, phrase).children).toBe('Hello, <mark>world</mark>!');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,42 @@
|
||||||
|
import listItems from '../../../../src/common/helpers/listItems';
|
||||||
|
import m from 'mithril';
|
||||||
|
|
||||||
|
describe('listItems', () => {
|
||||||
|
it('should return an array of Vnodes', () => {
|
||||||
|
const items = [m('div', 'Item 1'), m('div', 'Item 2'), m('div', 'Item 3')];
|
||||||
|
|
||||||
|
const result = listItems(items);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(3);
|
||||||
|
expect(result[0].tag).toBe('li');
|
||||||
|
expect(result[0].children[0].children[0].children).toBe('Item 1');
|
||||||
|
expect(result[1].children[0].children[0].children).toBe('Item 2');
|
||||||
|
expect(result[2].children[0].children[0].children).toBe('Item 3');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an array of Vnodes with custom tag', () => {
|
||||||
|
const items = [m('div', 'Item 1'), m('div', 'Item 2'), m('div', 'Item 3')];
|
||||||
|
|
||||||
|
const result = listItems(items, 'customTag');
|
||||||
|
|
||||||
|
expect(result).toHaveLength(3);
|
||||||
|
expect(result[0].tag).toBe('customTag');
|
||||||
|
expect(result[0].children[0].children[0].children).toBe('Item 1');
|
||||||
|
expect(result[1].children[0].children[0].children).toBe('Item 2');
|
||||||
|
expect(result[2].children[0].children[0].children).toBe('Item 3');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an array of Vnodes with custom tag and attributes', () => {
|
||||||
|
const items = [m('div', 'Item 1'), m('div', 'Item 2'), m('div', 'Item 3')];
|
||||||
|
|
||||||
|
const result = listItems(items, 'ul', { id: 'list' });
|
||||||
|
|
||||||
|
expect(result).toHaveLength(3);
|
||||||
|
expect(result[0].tag).toBe('ul');
|
||||||
|
// @ts-ignore
|
||||||
|
expect(result[0].attrs.id).toBe('list');
|
||||||
|
expect(result[0].children[0].children[0].children).toBe('Item 1');
|
||||||
|
expect(result[1].children[0].children[0].children).toBe('Item 2');
|
||||||
|
expect(result[2].children[0].children[0].children).toBe('Item 3');
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,5 +1,8 @@
|
||||||
|
import bootstrapForum from '@flarum/jest-config/src/boostrap/forum';
|
||||||
import abbreviateNumber from '../../../../src/common/utils/abbreviateNumber';
|
import abbreviateNumber from '../../../../src/common/utils/abbreviateNumber';
|
||||||
|
|
||||||
|
beforeAll(() => bootstrapForum());
|
||||||
|
|
||||||
test('does not change small numbers', () => {
|
test('does not change small numbers', () => {
|
||||||
expect(abbreviateNumber(1)).toBe('1');
|
expect(abbreviateNumber(1)).toBe('1');
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
import extractText from '../../../../src/common/utils/extractText';
|
||||||
|
|
||||||
|
describe('extractText', () => {
|
||||||
|
it('should extract text from a virtual element', () => {
|
||||||
|
const vdom = ['Hello, ', { tag: 'span', children: 'world' }, '!'];
|
||||||
|
// @ts-ignore
|
||||||
|
expect(extractText(vdom)).toBe('Hello, world!');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract text from a nested virtual element', () => {
|
||||||
|
const vdom = ['Hello, ', { tag: 'span', children: ['world', '!'] }];
|
||||||
|
// @ts-ignore
|
||||||
|
expect(extractText(vdom)).toBe('Hello, world!');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract text from an array of strings', () => {
|
||||||
|
const vdom = ['Hello, ', 'world', '!'];
|
||||||
|
expect(extractText(vdom)).toBe('Hello, world!');
|
||||||
|
});
|
||||||
|
});
|
28
framework/core/js/tests/unit/common/utils/string.test.ts
Normal file
28
framework/core/js/tests/unit/common/utils/string.test.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import * as string from '../../../../src/common/utils/string';
|
||||||
|
|
||||||
|
describe('string', () => {
|
||||||
|
it('should slugify a string', () => {
|
||||||
|
const stringToSlugify = 'Hello, world!';
|
||||||
|
expect(string.slug(stringToSlugify)).toBe('hello-world');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should slugify a string with a custom slugging mode', () => {
|
||||||
|
const stringToSlugify = 'Hello, world!';
|
||||||
|
expect(string.slug(stringToSlugify, string.SluggingMode.UTF8)).toBe('hello-world');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should slugify a string with a custom slugging mode', () => {
|
||||||
|
const stringToSlugify = 'Hello, world!';
|
||||||
|
expect(string.slug(stringToSlugify, string.SluggingMode.ALPHANUMERIC)).toBe('hello-world');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should make the first character of a string uppercase', () => {
|
||||||
|
const stringToUppercase = 'hello, world!';
|
||||||
|
expect(string.ucfirst(stringToUppercase)).toBe('Hello, world!');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transform a camel case string to snake case', () => {
|
||||||
|
const stringToTransform = 'helloWorld';
|
||||||
|
expect(string.camelCaseToSnakeCase(stringToTransform)).toBe('hello_world');
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,5 +1,10 @@
|
||||||
{
|
{
|
||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"include": ["tests/**/*"],
|
"include": ["tests/**/*"],
|
||||||
"files": ["../../../node_modules/@flarum/jest-config/shims.d.ts"]
|
"files": ["../../../node_modules/@flarum/jest-config/shims.d.ts"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": false,
|
||||||
|
"noImplicitReturns": false,
|
||||||
|
"noImplicitAny": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,15 +4,18 @@ This package provides a [Jest](https://jestjs.io/) config object to run unit & i
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
* Install the package: `yarn add --dev @flarum/jest-config`
|
- Install the package: `yarn add --dev @flarum/jest-config`
|
||||||
* Add `"type": "module"` to your `package.json`
|
- Add `"type": "module"` to your `package.json`
|
||||||
* Add `"test": "yarn node --experimental-vm-modules $(yarn bin jest)"` to your `package.json` scripts
|
- Add `"test": "yarn node --experimental-vm-modules $(yarn bin jest)"` to your `package.json` scripts
|
||||||
* Rename `webpack.config.js` to `webpack.config.cjs`
|
- Rename `webpack.config.js` to `webpack.config.cjs`
|
||||||
* Create a `jest.config.cjs` file with the following content:
|
- Create a `jest.config.cjs` file with the following content:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
module.exports = require('@flarum/jest-config')();
|
module.exports = require('@flarum/jest-config')();
|
||||||
```
|
```
|
||||||
* If you are using TypeScript, create `tsconfig.test.json` with the following content:
|
|
||||||
|
- If you are using TypeScript, create `tsconfig.test.json` with the following content:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
|
|
|
@ -4,10 +4,7 @@ module.exports = (options = {}) => ({
|
||||||
testEnvironment: 'jsdom',
|
testEnvironment: 'jsdom',
|
||||||
extensionsToTreatAsEsm: ['.ts', '.tsx'],
|
extensionsToTreatAsEsm: ['.ts', '.tsx'],
|
||||||
transform: {
|
transform: {
|
||||||
'^.+\\.[tj]sx?$': [
|
'^.+\\.[tj]sx?$': ['babel-jest', require('flarum-webpack-config/babel.config.cjs')],
|
||||||
'babel-jest',
|
|
||||||
require('flarum-webpack-config/babel.config.cjs'),
|
|
||||||
],
|
|
||||||
'^.+\\.tsx?$': [
|
'^.+\\.tsx?$': [
|
||||||
'ts-jest',
|
'ts-jest',
|
||||||
{
|
{
|
||||||
|
@ -16,6 +13,7 @@ module.exports = (options = {}) => ({
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
preset: 'ts-jest',
|
preset: 'ts-jest',
|
||||||
|
setupFiles: [path.resolve(__dirname, 'pollyfills.js')],
|
||||||
setupFilesAfterEnv: [path.resolve(__dirname, 'setup-env.js')],
|
setupFilesAfterEnv: [path.resolve(__dirname, 'setup-env.js')],
|
||||||
moduleDirectories: ['node_modules', 'src'],
|
moduleDirectories: ['node_modules', 'src'],
|
||||||
...options,
|
...options,
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
"jest": "^29.3.1",
|
"jest": "^29.3.1",
|
||||||
"jest-environment-jsdom": "^29.3.1",
|
"jest-environment-jsdom": "^29.3.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
|
"jsdom": "^24.0.0",
|
||||||
"mithril-query": "^4.0.1",
|
"mithril-query": "^4.0.1",
|
||||||
"ts-jest": "^29.0.3"
|
"ts-jest": "^29.0.3"
|
||||||
},
|
},
|
||||||
|
|
3
js-packages/jest-config/pollyfills.js
Normal file
3
js-packages/jest-config/pollyfills.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import { TextEncoder, TextDecoder } from 'util';
|
||||||
|
|
||||||
|
Object.assign(global, { TextDecoder, TextEncoder });
|
|
@ -1,54 +1,61 @@
|
||||||
import app from '@flarum/core/src/forum/app';
|
import mixin from '@flarum/core/src/common/utils/mixin';
|
||||||
import ForumApplication from '@flarum/core/src/forum/ForumApplication';
|
import ExportRegistry from '@flarum/core/src/common/ExportRegistry';
|
||||||
import jsYaml from 'js-yaml';
|
|
||||||
import fs from 'fs';
|
|
||||||
import jquery from 'jquery';
|
import jquery from 'jquery';
|
||||||
import m from 'mithril';
|
import m from 'mithril';
|
||||||
import flatten from 'flat';
|
import dayjs from 'dayjs';
|
||||||
import './test-matchers';
|
import './test-matchers';
|
||||||
|
|
||||||
// Boot the Flarum app.
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
function bootApp() {
|
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||||
ForumApplication.prototype.mount = () => {};
|
import jsdom from 'jsdom';
|
||||||
window.flarum = { extensions: {} };
|
|
||||||
app.load({
|
|
||||||
apiDocument: null,
|
|
||||||
locale: 'en',
|
|
||||||
locales: {},
|
|
||||||
resources: [
|
|
||||||
{
|
|
||||||
type: 'forums',
|
|
||||||
id: '1',
|
|
||||||
attributes: {
|
|
||||||
canEditUserCredentials: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'users',
|
|
||||||
id: '1',
|
|
||||||
attributes: {
|
|
||||||
id: 1,
|
|
||||||
username: 'admin',
|
|
||||||
displayName: 'Admin',
|
|
||||||
email: 'admin@machine.local',
|
|
||||||
joinTime: '2021-01-01T00:00:00Z',
|
|
||||||
isEmailConfirmed: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
session: {
|
|
||||||
userId: 1,
|
|
||||||
csrfToken: 'test',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
app.translator.addTranslations(flatten(jsYaml.load(fs.readFileSync('../locale/core.yml', 'utf8'))));
|
|
||||||
app.bootExtensions(window.flarum.extensions);
|
|
||||||
app.boot();
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeAll(() => {
|
dayjs.extend(relativeTime);
|
||||||
|
dayjs.extend(localizedFormat);
|
||||||
|
|
||||||
|
process.env.testing = true;
|
||||||
|
|
||||||
|
const dom = new jsdom.JSDOM('', {
|
||||||
|
pretendToBeVisual: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fill in the globals Mithril.js needs to operate. Also, the first two are often
|
||||||
|
// useful to have just in tests.
|
||||||
|
global.window = dom.window;
|
||||||
|
global.document = dom.window.document;
|
||||||
|
global.requestAnimationFrame = (callback) => callback();
|
||||||
|
|
||||||
|
// Some other needed pollyfills.
|
||||||
window.$ = jquery;
|
window.$ = jquery;
|
||||||
window.m = m;
|
window.m = m;
|
||||||
|
window.$.fn.tooltip = () => {};
|
||||||
bootApp();
|
window.matchMedia = () => ({
|
||||||
|
addListener: () => {},
|
||||||
|
removeListener: () => {},
|
||||||
|
});
|
||||||
|
window.scrollTo = () => {};
|
||||||
|
|
||||||
|
// Flarum specific globals.
|
||||||
|
global.flarum = {
|
||||||
|
extensions: {},
|
||||||
|
reg: new (mixin(ExportRegistry, {
|
||||||
|
checkModule: () => true,
|
||||||
|
}))(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prepare basic dom structure.
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<div id="app">
|
||||||
|
<main class="App-content">
|
||||||
|
<div id="notices"></div>
|
||||||
|
<div id="content"></div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
flarum.reg.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
dom.window.close();
|
||||||
});
|
});
|
||||||
|
|
2
js-packages/jest-config/shims.d.ts
vendored
2
js-packages/jest-config/shims.d.ts
vendored
|
@ -4,6 +4,8 @@ declare global {
|
||||||
namespace jest {
|
namespace jest {
|
||||||
interface Matchers<R> {
|
interface Matchers<R> {
|
||||||
toHaveElement(selector: any): R;
|
toHaveElement(selector: any): R;
|
||||||
|
toHaveElementAttr(selector: any, attribute: any, value: any): R;
|
||||||
|
toHaveElementAttr(selector: any, attribute: any): R;
|
||||||
toContainRaw(content: any): R;
|
toContainRaw(content: any): R;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
18
js-packages/jest-config/src/boostrap/admin.js
Normal file
18
js-packages/jest-config/src/boostrap/admin.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import app from '@flarum/core/src/admin/app';
|
||||||
|
import AdminApplication from '@flarum/core/src/admin/AdminApplication';
|
||||||
|
import bootstrap from './common.js';
|
||||||
|
|
||||||
|
export default function bootstrapAdmin(payload = {}) {
|
||||||
|
return bootstrap(AdminApplication, app, {
|
||||||
|
extensions: {},
|
||||||
|
settings: {},
|
||||||
|
permissions: {},
|
||||||
|
displayNameDrivers: [],
|
||||||
|
slugDrivers: {},
|
||||||
|
searchDrivers: {},
|
||||||
|
modelStatistics: {
|
||||||
|
users: 1,
|
||||||
|
},
|
||||||
|
...payload,
|
||||||
|
});
|
||||||
|
}
|
41
js-packages/jest-config/src/boostrap/common.js
Normal file
41
js-packages/jest-config/src/boostrap/common.js
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import Drawer from '@flarum/core/src/common/utils/Drawer';
|
||||||
|
import { makeUser } from '@flarum/core/tests/factory';
|
||||||
|
import flatten from 'flat';
|
||||||
|
import jsYaml from 'js-yaml';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
export default function bootstrap(Application, app, payload = {}) {
|
||||||
|
Application.prototype.mount = () => {};
|
||||||
|
|
||||||
|
app.load({
|
||||||
|
apiDocument: null,
|
||||||
|
locale: 'en',
|
||||||
|
locales: {},
|
||||||
|
resources: [
|
||||||
|
{
|
||||||
|
type: 'forums',
|
||||||
|
id: '1',
|
||||||
|
attributes: {
|
||||||
|
canEditUserCredentials: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
makeUser({
|
||||||
|
id: '1',
|
||||||
|
attributes: {
|
||||||
|
id: 1,
|
||||||
|
username: 'admin',
|
||||||
|
displayName: 'Admin',
|
||||||
|
email: 'admin@machine.local',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
session: {
|
||||||
|
userId: 1,
|
||||||
|
csrfToken: 'test',
|
||||||
|
},
|
||||||
|
...payload,
|
||||||
|
});
|
||||||
|
|
||||||
|
app.translator.addTranslations(flatten(jsYaml.load(fs.readFileSync('../locale/core.yml', 'utf8'))));
|
||||||
|
app.drawer = new Drawer();
|
||||||
|
}
|
7
js-packages/jest-config/src/boostrap/forum.js
Normal file
7
js-packages/jest-config/src/boostrap/forum.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import app from '@flarum/core/src/forum/app';
|
||||||
|
import ForumApplication from '@flarum/core/src/forum/ForumApplication';
|
||||||
|
import bootstrap from './common.js';
|
||||||
|
|
||||||
|
export default function bootstrapForum(payload = {}) {
|
||||||
|
return bootstrap(ForumApplication, app, payload);
|
||||||
|
}
|
|
@ -4,6 +4,19 @@ import { expect } from '@jest/globals';
|
||||||
expect.extend({
|
expect.extend({
|
||||||
toHaveElement: intoMatcher((out: any, expected: any) => out.should.have(expected), 'Expected $received to have node $expected'),
|
toHaveElement: intoMatcher((out: any, expected: any) => out.should.have(expected), 'Expected $received to have node $expected'),
|
||||||
toContainRaw: intoMatcher((out: any, expected: any) => out.should.contain(expected), 'Expected $received to contain $expected'),
|
toContainRaw: intoMatcher((out: any, expected: any) => out.should.contain(expected), 'Expected $received to contain $expected'),
|
||||||
|
toHaveElementAttr: intoMatcher(function (out: any, selector: string, attribute: string, value: string | undefined) {
|
||||||
|
out.should.have(selector);
|
||||||
|
|
||||||
|
const node = out.find(selector)[0];
|
||||||
|
|
||||||
|
const attr = node[attribute] ?? node._attrsByQName[attribute]?.data ?? undefined;
|
||||||
|
|
||||||
|
const onlyTwoArgs = value === undefined;
|
||||||
|
|
||||||
|
if (!node || (!onlyTwoArgs && attr !== value) || (onlyTwoArgs && !attr)) {
|
||||||
|
throw new Error(`Expected ${selector} to have attribute ${attribute} with value ${value}, but found ${node[attribute]}`);
|
||||||
|
}
|
||||||
|
}, 'Expected $received to have attribute $expected with value $value'),
|
||||||
});
|
});
|
||||||
|
|
||||||
function intoMatcher(callback: Function, message: string) {
|
function intoMatcher(callback: Function, message: string) {
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
{
|
{
|
||||||
"extends": "flarum-tsconfig",
|
"extends": "flarum-tsconfig"
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ yarn add -D @flarum/prettier-config
|
||||||
{
|
{
|
||||||
"name": "my-cool-package",
|
"name": "my-cool-package",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"prettier": "@flarum/prettier-config",
|
"prettier": "@flarum/prettier-config"
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
@ -4,4 +4,3 @@
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"trailingComma": "es5"
|
"trailingComma": "es5"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,16 @@
|
||||||
"target": "es6",
|
"target": "es6",
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"lib": ["dom", "es5", "es2015", "es2016", "es2017", "es2018", "es2019.array", "es2020"],
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"es5",
|
||||||
|
"es2015",
|
||||||
|
"es2016",
|
||||||
|
"es2017",
|
||||||
|
"es2018",
|
||||||
|
"es2019.array",
|
||||||
|
"es2020"
|
||||||
|
],
|
||||||
"allowSyntheticDefaultImports": true
|
"allowSyntheticDefaultImports": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,11 +11,13 @@ class OverrideChunkLoaderFunction {
|
||||||
// The function is called by webpack so we can't just override it.
|
// The function is called by webpack so we can't just override it.
|
||||||
compiler.hooks.compilation.tap('OverrideChunkLoaderFunction', (compilation) => {
|
compiler.hooks.compilation.tap('OverrideChunkLoaderFunction', (compilation) => {
|
||||||
compilation.mainTemplate.hooks.requireEnsure.tap('OverrideChunkLoaderFunction', (source) => {
|
compilation.mainTemplate.hooks.requireEnsure.tap('OverrideChunkLoaderFunction', (source) => {
|
||||||
return source + '\nconst originalLoadChunk = __webpack_require__.l;\n__webpack_require__.l = flarum.reg.loadChunk.bind(flarum.reg, originalLoadChunk);';
|
return (
|
||||||
|
source +
|
||||||
|
'\nconst originalLoadChunk = __webpack_require__.l;\n__webpack_require__.l = flarum.reg.loadChunk.bind(flarum.reg, originalLoadChunk);'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
module.exports = OverrideChunkLoaderFunction;
|
module.exports = OverrideChunkLoaderFunction;
|
||||||
|
|
|
@ -7,7 +7,7 @@ class RegisterAsyncChunksPlugin {
|
||||||
static registry = {};
|
static registry = {};
|
||||||
|
|
||||||
processUrlPath(urlPath) {
|
processUrlPath(urlPath) {
|
||||||
if (path.sep == "\\") {
|
if (path.sep == '\\') {
|
||||||
// separator on windows is "\", this will cause escape issues when used in url path.
|
// separator on windows is "\", this will cause escape issues when used in url path.
|
||||||
return urlPath.replace(/\\/g, '/');
|
return urlPath.replace(/\\/g, '/');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
const path = require("path");
|
const path = require('path');
|
||||||
const {getOptions} = require("loader-utils");
|
const { getOptions } = require('loader-utils');
|
||||||
const {validate} = require("schema-utils");
|
const { validate } = require('schema-utils');
|
||||||
const fs = require("fs");
|
const fs = require('fs');
|
||||||
|
|
||||||
const optionsSchema = {
|
const optionsSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
@ -66,7 +66,7 @@ module.exports = function autoChunkNameLoader(source) {
|
||||||
chunkPath = absolutePathToImport.split(`src${path.sep}`)[1];
|
chunkPath = absolutePathToImport.split(`src${path.sep}`)[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path.sep == "\\") {
|
if (path.sep == '\\') {
|
||||||
// separator on windows is '\', the resolver only works with '/'.
|
// separator on windows is '\', the resolver only works with '/'.
|
||||||
chunkPath = chunkPath.replace(/\\/g, '/');
|
chunkPath = chunkPath.replace(/\\/g, '/');
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,9 @@ module.exports = function autoChunkNameLoader(source) {
|
||||||
webpackMode: 'lazy-once',
|
webpackMode: 'lazy-once',
|
||||||
};
|
};
|
||||||
|
|
||||||
const comment = Object.entries(webpackCommentOptions).map(([key, value]) => `${key}: '${value}'`).join(', ');
|
const comment = Object.entries(webpackCommentOptions)
|
||||||
|
.map(([key, value]) => `${key}: '${value}'`)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
// Return the new import statement
|
// Return the new import statement
|
||||||
return `${pre}import(/* ${comment} */ '${relativePathToImport}')`;
|
return `${pre}import(/* ${comment} */ '${relativePathToImport}')`;
|
||||||
|
|
|
@ -98,7 +98,7 @@ function addAutoExports(source, pathToModule, moduleName) {
|
||||||
|
|
||||||
if (matches.length) {
|
if (matches.length) {
|
||||||
const map = matches.reduce((map, match) => {
|
const map = matches.reduce((map, match) => {
|
||||||
const names = match[1] ? match[1].split(',') : (match[2] ? [match[2]] : null);
|
const names = match[1] ? match[1].split(',') : match[2] ? [match[2]] : null;
|
||||||
|
|
||||||
if (!names) {
|
if (!names) {
|
||||||
return map;
|
return map;
|
||||||
|
@ -179,7 +179,8 @@ module.exports = function autoExportLoader(source) {
|
||||||
// Get the path of the module to be exported
|
// Get the path of the module to be exported
|
||||||
// relative to the src directory.
|
// relative to the src directory.
|
||||||
// Example: src/forum/components/UserCard.js => forum/components
|
// Example: src/forum/components/UserCard.js => forum/components
|
||||||
const pathToModule = path.relative(path.resolve(this.rootContext, 'src'), this.resourcePath)
|
const pathToModule = path
|
||||||
|
.relative(path.resolve(this.rootContext, 'src'), this.resourcePath)
|
||||||
.replaceAll(path.sep, '/')
|
.replaceAll(path.sep, '/')
|
||||||
.replace(/[A-Za-z_]+\.[A-Za-z_]+$/, '');
|
.replace(/[A-Za-z_]+\.[A-Za-z_]+$/, '');
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1 @@
|
||||||
module.exports = (name) => name === 'flarum/core'
|
module.exports = (name) => (name === 'flarum/core' ? 'core' : name.replace('/flarum-ext-', '-').replace('/flarum-', '').replace('/', '-'));
|
||||||
? 'core'
|
|
||||||
: name.replace('/flarum-ext-', '-').replace('/flarum-', '').replace('/', '-')
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { NormalModuleReplacementPlugin } = require('webpack');
|
const { NormalModuleReplacementPlugin } = require('webpack');
|
||||||
const RegisterAsyncChunksPlugin = require("./RegisterAsyncChunksPlugin.cjs");
|
const RegisterAsyncChunksPlugin = require('./RegisterAsyncChunksPlugin.cjs');
|
||||||
const OverrideChunkLoaderFunction = require("./OverrideChunkLoaderFunction.cjs");
|
const OverrideChunkLoaderFunction = require('./OverrideChunkLoaderFunction.cjs');
|
||||||
|
|
||||||
const entryPointNames = ['forum', 'admin'];
|
const entryPointNames = ['forum', 'admin'];
|
||||||
const entryPointExts = ['js', 'ts'];
|
const entryPointExts = ['js', 'ts'];
|
||||||
|
@ -106,8 +106,8 @@ module.exports = function () {
|
||||||
cacheGroups: {
|
cacheGroups: {
|
||||||
// Avoid node_modules being split into separate chunks
|
// Avoid node_modules being split into separate chunks
|
||||||
defaultVendors: false,
|
defaultVendors: false,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
output: {
|
output: {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user