mirror of
https://github.com/flarum/framework.git
synced 2024-11-22 04:25:31 +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.
|
||||
type: number
|
||||
required: false
|
||||
default: 16
|
||||
default: 20
|
||||
|
||||
js_package_manager:
|
||||
description: "Enable TypeScript?"
|
||||
|
@ -142,7 +142,7 @@ jobs:
|
|||
working-directory: ${{ inputs.frontend_directory }}
|
||||
|
||||
- name: JS Checks & Production Build
|
||||
uses: flarum/action-build@v3
|
||||
uses: flarum/action-build@v4
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
build_script: ${{ inputs.build_script }}
|
||||
|
|
|
@ -93,7 +93,7 @@ class ListTest extends TestCase
|
|||
$data = json_decode($body, true);
|
||||
|
||||
$tagIds = array_map(function ($tag) {
|
||||
return $tag['id'];
|
||||
return (int) $tag['id'];
|
||||
}, array_filter($data['included'], function ($item) {
|
||||
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] {
|
||||
// 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);
|
||||
|
|
|
@ -36,7 +36,7 @@ export default class Avatar<CustomAttrs extends IAvatarAttrs = IAvatarAttrs> ext
|
|||
}
|
||||
|
||||
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>;
|
||||
|
|
|
@ -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 newStyle = { ...style, '--badge-bg': color };
|
||||
const newStyle = { ...style };
|
||||
|
||||
if (!process.env.testing) {
|
||||
newStyle['--badge-bg'] = color;
|
||||
}
|
||||
|
||||
const badgeAttrs = {
|
||||
...attrs,
|
||||
|
|
|
@ -1,13 +1,22 @@
|
|||
import type Mithril from 'mithril';
|
||||
|
||||
import Component, { ComponentAttrs } from '../Component';
|
||||
import classList from '../utils/classList';
|
||||
|
||||
import Icon from './Icon';
|
||||
|
||||
export default class ColorPreviewInput extends Component {
|
||||
view(vnode: Mithril.Vnode<ComponentAttrs, this>) {
|
||||
const { className, id, ...attrs } = this.attrs;
|
||||
export interface IColorPreviewInputAttrs extends ComponentAttrs {
|
||||
value: string;
|
||||
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';
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Component, { ComponentAttrs } from '../Component';
|
||||
import listItems from '../helpers/listItems';
|
||||
import classList from '../utils/classList';
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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
|
||||
*/
|
||||
|
@ -101,7 +101,7 @@ export interface SelectFieldComponentOptions extends CommonFieldOptions {
|
|||
* Valid options for the setting component builder to generate a Textarea.
|
||||
*/
|
||||
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)) {
|
||||
settingElement = <textarea id={inputId} aria-describedby={helpTextId} bidi={stream} {...attrs} />;
|
||||
} else {
|
||||
let Tag: VnodeElementTag = 'input';
|
||||
let Tag: VnodeElementTag | typeof ColorPreviewInput = 'input';
|
||||
|
||||
if (type === ColorPreviewSettingType) {
|
||||
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() {
|
||||
if (this.alertAttrs) {
|
||||
this.alertAttrs.dismissible = false;
|
||||
|
|
|
@ -43,7 +43,7 @@ export default class ModalManager extends Component<IModalManagerAttrs> {
|
|||
data-modal-number={i}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
style={{ '--modal-number': i }}
|
||||
style={!process.env.testing && { '--modal-number': i }}
|
||||
aria-hidden={this.attrs.state.modal !== modal && 'true'}
|
||||
>
|
||||
{!!Tag && [
|
||||
|
@ -66,7 +66,7 @@ export default class ModalManager extends Component<IModalManagerAttrs> {
|
|||
className="Modal-backdrop backdrop"
|
||||
ontransitionend={this.onBackdropTransitionEnd.bind(this)}
|
||||
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 />}
|
||||
</div>
|
||||
|
@ -122,7 +122,9 @@ export default class ModalManager extends Component<IModalManagerAttrs> {
|
|||
this.focusTrap = createFocusTrap(this.activeDialogElement as HTMLElement, { allowOutsideClick: true });
|
||||
this.focusTrap!.activate?.();
|
||||
|
||||
disableBodyScroll(this.activeDialogManagerElement!, { reserveScrollBarGap: true });
|
||||
if (this.activeDialogManagerElement) {
|
||||
disableBodyScroll(this.activeDialogManagerElement, { reserveScrollBarGap: true });
|
||||
}
|
||||
}
|
||||
|
||||
// Update key of current opened modal
|
||||
|
@ -136,21 +138,21 @@ export default class ModalManager extends Component<IModalManagerAttrs> {
|
|||
/**
|
||||
* Get current active dialog
|
||||
*/
|
||||
private get activeDialogElement(): HTMLElement {
|
||||
return document.body.querySelector(`.ModalManager[data-modal-key="${this.attrs.state.modal?.key}"] .Modal`) as HTMLElement;
|
||||
private get activeDialogElement(): HTMLElement | null {
|
||||
return document.body.querySelector(`.ModalManager[data-modal-key="${this.attrs.state.modal?.key}"] .Modal`) as HTMLElement | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current active dialog
|
||||
*/
|
||||
private get activeDialogManagerElement(): HTMLElement {
|
||||
return document.body.querySelector(`.ModalManager[data-modal-key="${this.attrs.state.modal?.key}"]`) as HTMLElement;
|
||||
private get activeDialogManagerElement(): HTMLElement | null {
|
||||
return document.body.querySelector(`.ModalManager[data-modal-key="${this.attrs.state.modal?.key}"]`) as HTMLElement | null;
|
||||
}
|
||||
|
||||
animateShow(readyCallback: () => void = () => {}): void {
|
||||
if (!this.attrs.state.modal) return;
|
||||
|
||||
this.activeDialogElement.addEventListener(
|
||||
this.activeDialogElement?.addEventListener(
|
||||
'transitionend',
|
||||
() => {
|
||||
readyCallback();
|
||||
|
@ -159,7 +161,7 @@ export default class ModalManager extends Component<IModalManagerAttrs> {
|
|||
);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.activeDialogElement.classList.add('in');
|
||||
this.activeDialogElement?.classList.add('in');
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -176,10 +178,10 @@ export default class ModalManager extends Component<IModalManagerAttrs> {
|
|||
closedCallback();
|
||||
};
|
||||
|
||||
this.activeDialogElement.addEventListener('transitionend', afterModalClosedCallback, { once: true });
|
||||
this.activeDialogElement?.addEventListener('transitionend', afterModalClosedCallback, { once: true });
|
||||
|
||||
this.activeDialogElement.classList.remove('in');
|
||||
this.activeDialogElement.classList.add('out');
|
||||
this.activeDialogElement?.classList.remove('in');
|
||||
this.activeDialogElement?.classList.add('out');
|
||||
}
|
||||
|
||||
protected handleEscPress(e: KeyboardEvent): void {
|
||||
|
|
|
@ -1,38 +1,14 @@
|
|||
import app from '../app';
|
||||
import Component, { type ComponentAttrs } from '../Component';
|
||||
import classList from '../utils/classList';
|
||||
import Dropdown from './Dropdown';
|
||||
import Mithril from 'mithril';
|
||||
import Button from './Button';
|
||||
import Tooltip from './Tooltip';
|
||||
import Select, { ISelectAttrs, Option } from './Select';
|
||||
|
||||
export type Option = {
|
||||
label: string;
|
||||
disabled?: boolean | ((value: string[]) => boolean);
|
||||
tooltip?: string;
|
||||
};
|
||||
export interface IMultiSelectAttrs extends ISelectAttrs {}
|
||||
|
||||
export interface IMultiSelectAttrs extends ComponentAttrs {
|
||||
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> {
|
||||
export default class MultiSelect<CustomAttrs extends IMultiSelectAttrs = IMultiSelectAttrs> extends Select<CustomAttrs> {
|
||||
protected selected: string[] = [];
|
||||
|
||||
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||
|
@ -41,7 +17,7 @@ export default class MultiSelect<CustomAttrs extends IMultiSelectAttrs = IMultiS
|
|||
this.selected = this.attrs.value || [];
|
||||
}
|
||||
|
||||
view() {
|
||||
input(): JSX.Element {
|
||||
const {
|
||||
options,
|
||||
onchange,
|
||||
|
@ -49,56 +25,50 @@ export default class MultiSelect<CustomAttrs extends IMultiSelectAttrs = IMultiS
|
|||
className,
|
||||
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
|
||||
} = this.attrs;
|
||||
|
||||
return (
|
||||
<span className={classList('Select MultiSelect', wrapperClassName, wrapperClass)} {...wrapperAttrs}>
|
||||
<Dropdown
|
||||
disabled={disabled}
|
||||
buttonClassName="Button"
|
||||
buttonAttrs={{ disabled }}
|
||||
label={
|
||||
Object.keys(options)
|
||||
.filter((key) => this.selected.includes(key))
|
||||
.map((key) => (typeof options[key] === 'string' ? options[key] : (options[key] as Option).label))
|
||||
.join(', ') || app.translator.trans('core.lib.multi_select.placeholder')
|
||||
<Dropdown
|
||||
disabled={disabled}
|
||||
buttonClassName="Button"
|
||||
buttonAttrs={{ disabled }}
|
||||
label={
|
||||
Object.keys(options)
|
||||
.filter((key) => this.selected.includes(key))
|
||||
.map((key) => (typeof options[key] === 'string' ? options[key] : (options[key] as Option).label))
|
||||
.join(', ') || app.translator.trans('core.lib.multi_select.placeholder')
|
||||
}
|
||||
>
|
||||
{Object.keys(options).map((key) => {
|
||||
const option = options[key];
|
||||
const label = typeof option === 'string' ? option : option.label;
|
||||
const tooltip = typeof option !== 'string' && option.tooltip;
|
||||
let disabled = typeof option !== 'string' && option.disabled;
|
||||
|
||||
if (typeof disabled === 'function') {
|
||||
disabled = disabled(this.selected);
|
||||
}
|
||||
>
|
||||
{Object.keys(options).map((key) => {
|
||||
const option = options[key];
|
||||
const label = typeof option === 'string' ? option : option.label;
|
||||
const tooltip = typeof option !== 'string' && option.tooltip;
|
||||
let disabled = typeof option !== 'string' && option.disabled;
|
||||
|
||||
if (typeof disabled === 'function') {
|
||||
disabled = disabled(this.selected);
|
||||
}
|
||||
const button = (
|
||||
<Button
|
||||
type="button"
|
||||
className={classList('Dropdown-item', `Dropdown-item--${key}`, { disabled })}
|
||||
onclick={this.toggle.bind(this, key)}
|
||||
disabled={disabled}
|
||||
icon={this.selected.includes(key) ? 'fas fa-check' : 'fas fa-empty'}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
|
||||
const button = (
|
||||
<Button
|
||||
type="button"
|
||||
className={classList('Dropdown-item', { disabled })}
|
||||
onclick={this.toggle.bind(this, key)}
|
||||
disabled={disabled}
|
||||
icon={this.selected.includes(key) ? 'fas fa-check' : 'fas fa-empty'}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
if (tooltip) {
|
||||
return <Tooltip text={tooltip}>{button}</Tooltip>;
|
||||
}
|
||||
|
||||
if (tooltip) {
|
||||
return <Tooltip text={tooltip}>{button}</Tooltip>;
|
||||
}
|
||||
|
||||
return button;
|
||||
})}
|
||||
</Dropdown>
|
||||
</span>
|
||||
return button;
|
||||
})}
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,22 @@
|
|||
import Component from '../Component';
|
||||
import Component, { type ComponentAttrs } from '../Component';
|
||||
import withAttr from '../utils/withAttr';
|
||||
import classList from '../utils/classList';
|
||||
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
|
||||
* 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.
|
||||
*/
|
||||
export default class Select extends Component {
|
||||
export default class Select<CustomAttrs extends ISelectAttrs = ISelectAttrs> extends Component<CustomAttrs> {
|
||||
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 {
|
||||
options,
|
||||
onchange,
|
||||
|
@ -25,15 +53,11 @@ export default class Select extends Component {
|
|||
className,
|
||||
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
|
||||
} = this.attrs;
|
||||
|
||||
return (
|
||||
<span className={classList('Select', wrapperClassName, wrapperClass)} {...wrapperAttrs}>
|
||||
<>
|
||||
<select
|
||||
className={classList('Select-input FormControl', className, _class)}
|
||||
onchange={onchange ? withAttr('value', onchange.bind(this)) : undefined}
|
||||
|
@ -43,15 +67,11 @@ export default class Select extends Component {
|
|||
>
|
||||
{Object.keys(options).map((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;
|
||||
let disabled = false;
|
||||
|
||||
if (typeof option === 'object' && option.label) {
|
||||
label = option.label;
|
||||
disabled = option.disabled ?? false;
|
||||
} else {
|
||||
label = option;
|
||||
if (typeof disabled === 'function') {
|
||||
disabled = disabled(value ?? null);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -62,7 +82,7 @@ export default class Select extends Component {
|
|||
})}
|
||||
</select>
|
||||
<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',
|
||||
UTF8 = 'utf8',
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import ItemList from '../../common/utils/ItemList';
|
|||
import Tooltip from '../../common/components/Tooltip';
|
||||
import HeaderList from './HeaderList';
|
||||
import HeaderListGroup from './HeaderListGroup';
|
||||
import NotificationType from './NotificationType';
|
||||
|
||||
/**
|
||||
* The `NotificationList` component displays a list of the logged-in user's
|
||||
|
@ -116,12 +117,7 @@ export default class NotificationList extends Component {
|
|||
)
|
||||
}
|
||||
>
|
||||
{group.notifications
|
||||
.map((notification) => {
|
||||
const NotificationComponent = app.notificationComponents[notification.contentType()];
|
||||
return !!NotificationComponent ? <NotificationComponent notification={notification} /> : null;
|
||||
})
|
||||
.filter((component) => !!component)}
|
||||
{group.notifications.map((notification) => <NotificationType notification={notification} />).filter((component) => !!component)}
|
||||
</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 Button from '../../common/components/Button';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import PostType from './PostType';
|
||||
|
||||
/**
|
||||
* The `PostStream` component displays an infinitely-scrollable wall of posts in
|
||||
|
@ -47,8 +48,7 @@ export default class PostStream extends Component {
|
|||
|
||||
if (post) {
|
||||
const time = post.createdAt();
|
||||
const PostComponent = app.postComponents[post.contentType()];
|
||||
content = !!PostComponent && <PostComponent post={post} />;
|
||||
content = <PostType post={post} />;
|
||||
|
||||
attrs.key = 'post' + post.id();
|
||||
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();
|
||||
|
||||
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="container">
|
||||
<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 m from 'mithril';
|
||||
import mq from 'mithril-query';
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
beforeAll(() => bootstrapForum());
|
||||
|
||||
describe('Alert displays as expected', () => {
|
||||
it('should display alert messages with an icon', () => {
|
||||
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';
|
||||
|
||||
beforeAll(() => bootstrapForum());
|
||||
|
||||
test('does not change small numbers', () => {
|
||||
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",
|
||||
"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
|
||||
|
||||
* Install the package: `yarn add --dev @flarum/jest-config`
|
||||
* Add `"type": "module"` to your `package.json`
|
||||
* Add `"test": "yarn node --experimental-vm-modules $(yarn bin jest)"` to your `package.json` scripts
|
||||
* Rename `webpack.config.js` to `webpack.config.cjs`
|
||||
* Create a `jest.config.cjs` file with the following content:
|
||||
- Install the package: `yarn add --dev @flarum/jest-config`
|
||||
- Add `"type": "module"` to your `package.json`
|
||||
- Add `"test": "yarn node --experimental-vm-modules $(yarn bin jest)"` to your `package.json` scripts
|
||||
- Rename `webpack.config.js` to `webpack.config.cjs`
|
||||
- Create a `jest.config.cjs` file with the following content:
|
||||
|
||||
```js
|
||||
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
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
|
|
|
@ -4,10 +4,7 @@ module.exports = (options = {}) => ({
|
|||
testEnvironment: 'jsdom',
|
||||
extensionsToTreatAsEsm: ['.ts', '.tsx'],
|
||||
transform: {
|
||||
'^.+\\.[tj]sx?$': [
|
||||
'babel-jest',
|
||||
require('flarum-webpack-config/babel.config.cjs'),
|
||||
],
|
||||
'^.+\\.[tj]sx?$': ['babel-jest', require('flarum-webpack-config/babel.config.cjs')],
|
||||
'^.+\\.tsx?$': [
|
||||
'ts-jest',
|
||||
{
|
||||
|
@ -16,6 +13,7 @@ module.exports = (options = {}) => ({
|
|||
],
|
||||
},
|
||||
preset: 'ts-jest',
|
||||
setupFiles: [path.resolve(__dirname, 'pollyfills.js')],
|
||||
setupFilesAfterEnv: [path.resolve(__dirname, 'setup-env.js')],
|
||||
moduleDirectories: ['node_modules', 'src'],
|
||||
...options,
|
||||
|
|
|
@ -1,35 +1,36 @@
|
|||
{
|
||||
"name": "@flarum/jest-config",
|
||||
"version": "1.0.1",
|
||||
"description": "Jest config for Flarum.",
|
||||
"main": "index.cjs",
|
||||
"author": "Flarum Team",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"prettier": "@flarum/prettier-config",
|
||||
"dependencies": {
|
||||
"@types/jest": "^29.2.2",
|
||||
"flarum-webpack-config": "^3.0.0",
|
||||
"flat": "^5.0.2",
|
||||
"jest": "^29.3.1",
|
||||
"jest-environment-jsdom": "^29.3.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"mithril-query": "^4.0.1",
|
||||
"ts-jest": "^29.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^2.4.1"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "echo 'skipping..'",
|
||||
"build": "echo 'skipping..'",
|
||||
"analyze": "echo 'skipping..'",
|
||||
"format": "prettier --write .",
|
||||
"format-check": "prettier --check .",
|
||||
"clean-typings": "echo 'skipping..'",
|
||||
"build-typings": "echo 'skipping..'",
|
||||
"post-build-typings": "echo 'skipping..'",
|
||||
"check-typings": "echo 'skipping..'",
|
||||
"check-typings-coverage": "echo 'skipping..'"
|
||||
}
|
||||
"name": "@flarum/jest-config",
|
||||
"version": "1.0.1",
|
||||
"description": "Jest config for Flarum.",
|
||||
"main": "index.cjs",
|
||||
"author": "Flarum Team",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"prettier": "@flarum/prettier-config",
|
||||
"dependencies": {
|
||||
"@types/jest": "^29.2.2",
|
||||
"flarum-webpack-config": "^3.0.0",
|
||||
"flat": "^5.0.2",
|
||||
"jest": "^29.3.1",
|
||||
"jest-environment-jsdom": "^29.3.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsdom": "^24.0.0",
|
||||
"mithril-query": "^4.0.1",
|
||||
"ts-jest": "^29.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^2.4.1"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "echo 'skipping..'",
|
||||
"build": "echo 'skipping..'",
|
||||
"analyze": "echo 'skipping..'",
|
||||
"format": "prettier --write .",
|
||||
"format-check": "prettier --check .",
|
||||
"clean-typings": "echo 'skipping..'",
|
||||
"build-typings": "echo 'skipping..'",
|
||||
"post-build-typings": "echo 'skipping..'",
|
||||
"check-typings": "echo 'skipping..'",
|
||||
"check-typings-coverage": "echo 'skipping..'"
|
||||
}
|
||||
}
|
||||
|
|
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 ForumApplication from '@flarum/core/src/forum/ForumApplication';
|
||||
import jsYaml from 'js-yaml';
|
||||
import fs from 'fs';
|
||||
import mixin from '@flarum/core/src/common/utils/mixin';
|
||||
import ExportRegistry from '@flarum/core/src/common/ExportRegistry';
|
||||
import jquery from 'jquery';
|
||||
import m from 'mithril';
|
||||
import flatten from 'flat';
|
||||
import dayjs from 'dayjs';
|
||||
import './test-matchers';
|
||||
|
||||
// Boot the Flarum app.
|
||||
function bootApp() {
|
||||
ForumApplication.prototype.mount = () => {};
|
||||
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();
|
||||
}
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
import jsdom from 'jsdom';
|
||||
|
||||
beforeAll(() => {
|
||||
window.$ = jquery;
|
||||
window.m = m;
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
bootApp();
|
||||
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.m = m;
|
||||
window.$.fn.tooltip = () => {};
|
||||
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 {
|
||||
interface Matchers<R> {
|
||||
toHaveElement(selector: any): R;
|
||||
toHaveElementAttr(selector: any, attribute: any, value: any): R;
|
||||
toHaveElementAttr(selector: any, attribute: 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({
|
||||
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'),
|
||||
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) {
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"extends": "flarum-tsconfig",
|
||||
"extends": "flarum-tsconfig"
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ yarn add -D @flarum/prettier-config
|
|||
{
|
||||
"name": "my-cool-package",
|
||||
"version": "1.0.0",
|
||||
"prettier": "@flarum/prettier-config",
|
||||
"prettier": "@flarum/prettier-config"
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
|
|
@ -1,35 +1,35 @@
|
|||
{
|
||||
"name": "@flarum/prettier-config",
|
||||
"version": "1.0.0",
|
||||
"description": "Flarum's configuration for the Prettier code formatter.",
|
||||
"main": "prettierrc.json",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"dev": "echo 'skipping..'",
|
||||
"build": "echo 'skipping..'",
|
||||
"analyze": "echo 'skipping..'",
|
||||
"format": "prettier --write .",
|
||||
"format-check": "prettier --check .",
|
||||
"clean-typings": "echo 'skipping..'",
|
||||
"build-typings": "echo 'skipping..'",
|
||||
"post-build-typings": "echo 'skipping..'",
|
||||
"check-typings": "echo 'skipping..'",
|
||||
"check-typings-coverage": "echo 'skipping..'"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/flarum/prettier-config.git"
|
||||
},
|
||||
"keywords": [
|
||||
"flarum"
|
||||
],
|
||||
"author": "Flarum Team",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/flarum/prettier-config/issues"
|
||||
},
|
||||
"homepage": "https://github.com/flarum/prettier-config#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
"name": "@flarum/prettier-config",
|
||||
"version": "1.0.0",
|
||||
"description": "Flarum's configuration for the Prettier code formatter.",
|
||||
"main": "prettierrc.json",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"dev": "echo 'skipping..'",
|
||||
"build": "echo 'skipping..'",
|
||||
"analyze": "echo 'skipping..'",
|
||||
"format": "prettier --write .",
|
||||
"format-check": "prettier --check .",
|
||||
"clean-typings": "echo 'skipping..'",
|
||||
"build-typings": "echo 'skipping..'",
|
||||
"post-build-typings": "echo 'skipping..'",
|
||||
"check-typings": "echo 'skipping..'",
|
||||
"check-typings-coverage": "echo 'skipping..'"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/flarum/prettier-config.git"
|
||||
},
|
||||
"keywords": [
|
||||
"flarum"
|
||||
],
|
||||
"author": "Flarum Team",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/flarum/prettier-config/issues"
|
||||
},
|
||||
"homepage": "https://github.com/flarum/prettier-config#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,4 +4,3 @@
|
|||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
{
|
||||
"name": "flarum-tsconfig",
|
||||
"version": "1.0.2",
|
||||
"description": "Flarum's official Typescript config file",
|
||||
"main": "tsconfig.json",
|
||||
"repository": "https://github.com/flarum/flarum-tsconfig",
|
||||
"author": "Flarum Team",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/jquery": "^3.5.5",
|
||||
"@types/mithril": "^2.0.7",
|
||||
"@types/throttle-debounce": "^2.1.0",
|
||||
"dayjs": "^1.10.4"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "echo 'skipping..'",
|
||||
"build": "echo 'skipping..'",
|
||||
"analyze": "echo 'skipping..'",
|
||||
"format": "prettier --write .",
|
||||
"format-check": "prettier --check .",
|
||||
"clean-typings": "echo 'skipping..'",
|
||||
"build-typings": "echo 'skipping..'",
|
||||
"post-build-typings": "echo 'skipping..'",
|
||||
"check-typings": "echo 'skipping..'",
|
||||
"check-typings-coverage": "echo 'skipping..'"
|
||||
}
|
||||
"name": "flarum-tsconfig",
|
||||
"version": "1.0.2",
|
||||
"description": "Flarum's official Typescript config file",
|
||||
"main": "tsconfig.json",
|
||||
"repository": "https://github.com/flarum/flarum-tsconfig",
|
||||
"author": "Flarum Team",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/jquery": "^3.5.5",
|
||||
"@types/mithril": "^2.0.7",
|
||||
"@types/throttle-debounce": "^2.1.0",
|
||||
"dayjs": "^1.10.4"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "echo 'skipping..'",
|
||||
"build": "echo 'skipping..'",
|
||||
"analyze": "echo 'skipping..'",
|
||||
"format": "prettier --write .",
|
||||
"format-check": "prettier --check .",
|
||||
"clean-typings": "echo 'skipping..'",
|
||||
"build-typings": "echo 'skipping..'",
|
||||
"post-build-typings": "echo 'skipping..'",
|
||||
"check-typings": "echo 'skipping..'",
|
||||
"check-typings-coverage": "echo 'skipping..'"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,16 @@
|
|||
"target": "es6",
|
||||
"jsx": "preserve",
|
||||
"allowJs": true,
|
||||
"lib": ["dom", "es5", "es2015", "es2016", "es2017", "es2018", "es2019.array", "es2020"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"es5",
|
||||
"es2015",
|
||||
"es2016",
|
||||
"es2017",
|
||||
"es2018",
|
||||
"es2019.array",
|
||||
"es2020"
|
||||
],
|
||||
"allowSyntheticDefaultImports": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,11 +11,13 @@ class OverrideChunkLoaderFunction {
|
|||
// The function is called by webpack so we can't just override it.
|
||||
compiler.hooks.compilation.tap('OverrideChunkLoaderFunction', (compilation) => {
|
||||
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;
|
||||
|
|
|
@ -7,7 +7,7 @@ class RegisterAsyncChunksPlugin {
|
|||
static registry = {};
|
||||
|
||||
processUrlPath(urlPath) {
|
||||
if (path.sep == "\\") {
|
||||
if (path.sep == '\\') {
|
||||
// separator on windows is "\", this will cause escape issues when used in url path.
|
||||
return urlPath.replace(/\\/g, '/');
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const path = require("path");
|
||||
const {getOptions} = require("loader-utils");
|
||||
const {validate} = require("schema-utils");
|
||||
const fs = require("fs");
|
||||
const path = require('path');
|
||||
const { getOptions } = require('loader-utils');
|
||||
const { validate } = require('schema-utils');
|
||||
const fs = require('fs');
|
||||
|
||||
const optionsSchema = {
|
||||
type: 'object',
|
||||
|
@ -66,7 +66,7 @@ module.exports = function autoChunkNameLoader(source) {
|
|||
chunkPath = absolutePathToImport.split(`src${path.sep}`)[1];
|
||||
}
|
||||
|
||||
if (path.sep == "\\") {
|
||||
if (path.sep == '\\') {
|
||||
// separator on windows is '\', the resolver only works with '/'.
|
||||
chunkPath = chunkPath.replace(/\\/g, '/');
|
||||
}
|
||||
|
@ -76,7 +76,9 @@ module.exports = function autoChunkNameLoader(source) {
|
|||
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 `${pre}import(/* ${comment} */ '${relativePathToImport}')`;
|
||||
|
|
|
@ -98,7 +98,7 @@ function addAutoExports(source, pathToModule, moduleName) {
|
|||
|
||||
if (matches.length) {
|
||||
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) {
|
||||
return map;
|
||||
|
@ -179,7 +179,8 @@ module.exports = function autoExportLoader(source) {
|
|||
// Get the path of the module to be exported
|
||||
// relative to the src directory.
|
||||
// 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, '/')
|
||||
.replace(/[A-Za-z_]+\.[A-Za-z_]+$/, '');
|
||||
|
||||
|
|
|
@ -1,3 +1 @@
|
|||
module.exports = (name) => name === 'flarum/core'
|
||||
? 'core'
|
||||
: name.replace('/flarum-ext-', '-').replace('/flarum-', '').replace('/', '-')
|
||||
module.exports = (name) => (name === 'flarum/core' ? 'core' : name.replace('/flarum-ext-', '-').replace('/flarum-', '').replace('/', '-'));
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { NormalModuleReplacementPlugin } = require('webpack');
|
||||
const RegisterAsyncChunksPlugin = require("./RegisterAsyncChunksPlugin.cjs");
|
||||
const OverrideChunkLoaderFunction = require("./OverrideChunkLoaderFunction.cjs");
|
||||
const RegisterAsyncChunksPlugin = require('./RegisterAsyncChunksPlugin.cjs');
|
||||
const OverrideChunkLoaderFunction = require('./OverrideChunkLoaderFunction.cjs');
|
||||
|
||||
const entryPointNames = ['forum', 'admin'];
|
||||
const entryPointExts = ['js', 'ts'];
|
||||
|
@ -106,8 +106,8 @@ module.exports = function () {
|
|||
cacheGroups: {
|
||||
// Avoid node_modules being split into separate chunks
|
||||
defaultVendors: false,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
output: {
|
||||
|
|
Loading…
Reference in New Issue
Block a user