From 653c968a3e5407004db9e0d8dbed9b3fe1133ed4 Mon Sep 17 00:00:00 2001 From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com> Date: Tue, 20 Jun 2023 16:00:37 +0200 Subject: [PATCH 01/20] Add missing FormattedNumber types --- src/components/number.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/components/number.ts b/src/components/number.ts index 0e4c8b6..f6477c3 100644 --- a/src/components/number.ts +++ b/src/components/number.ts @@ -1,6 +1,10 @@ import { defineSimpleFormatterComponent, definePartsFormatterComponent, + type SimpleFormatterComponentSlots, + type SimpleFormatterComponentProps, + type PartsFormatterComponentProps, + type PartsFormatterComponentSlots, } from './utils/index.ts' // This is required so that TypeScript cannot infer the types. @@ -8,6 +12,16 @@ import type {} from '@formatjs/ecma402-abstract' export const FormattedNumber = defineSimpleFormatterComponent('formatNumber') +export type FormattedNumberProps = SimpleFormatterComponentProps<'formatNumber'> + +export type FormattedNumberSlots = SimpleFormatterComponentSlots<'formatNumber'> + export const FormattedNumberParts = definePartsFormatterComponent( 'formatNumberToParts', ) + +export type FormattedNumberPartsProps = + PartsFormatterComponentProps<'formatNumberToParts'> + +export type FormattedNumberPartsSlots = + PartsFormatterComponentSlots<'formatNumberToParts'> From 86f54ae86afac10046499e50c10edf391e3de7a0 Mon Sep 17 00:00:00 2001 From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com> Date: Tue, 20 Jun 2023 19:52:24 +0200 Subject: [PATCH 02/20] Make Fragment component stateful --- src/components/fragment.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/fragment.ts b/src/components/fragment.ts index 18074f6..355bb11 100644 --- a/src/components/fragment.ts +++ b/src/components/fragment.ts @@ -1,9 +1,9 @@ -import type { VNode } from 'vue' +import { defineComponent, type VNode } from 'vue' interface FragmentProps { of: VNode[] | VNode } -export function Fragment(props: FragmentProps) { - return Array.isArray(props.of) ? props.of : [props.of] -} +export const Fragment = defineComponent( + (props: FragmentProps) => () => props.of, +) From d338ea485a67f8867ffacf7a7d1240eb9e6edde8 Mon Sep 17 00:00:00 2001 From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com> Date: Tue, 20 Jun 2023 20:27:00 +0200 Subject: [PATCH 03/20] Make Fragment component accept VNodeChild --- src/components/fragment.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/fragment.ts b/src/components/fragment.ts index 355bb11..9504771 100644 --- a/src/components/fragment.ts +++ b/src/components/fragment.ts @@ -1,9 +1,13 @@ -import { defineComponent, type VNode } from 'vue' +import { defineComponent, type VNodeChild } from 'vue' interface FragmentProps { - of: VNode[] | VNode + of: VNodeChild } export const Fragment = defineComponent( (props: FragmentProps) => () => props.of, + { + // eslint-disable-next-line vue/require-prop-types + props: ['of'], + }, ) From 9a05ff2109db39654d71f7a6a0f12a66b1c04848 Mon Sep 17 00:00:00 2001 From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com> Date: Tue, 20 Jun 2023 22:05:01 +0200 Subject: [PATCH 04/20] Fix lists component type --- src/components/lists.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/lists.ts b/src/components/lists.ts index 5af5f89..fa68e1d 100644 --- a/src/components/lists.ts +++ b/src/components/lists.ts @@ -14,11 +14,12 @@ interface FormattedListDefinedProps { items: readonly Item[] } -export interface FormattedListProps - extends FormattedListDefinedProps, - FormatListOptions {} +export type FormattedListProps = + FormattedListDefinedProps & FormatListOptions -export interface FormattedListSlots { +export interface FormattedListSlots< + Item extends string | VNode = string | VNode, +> { default(props: { children: Item extends string ? string : string | Item | (string | Item)[] }): any @@ -56,4 +57,5 @@ export const FormattedList = defineComponent( }, }, }, -) +) as (props: FormattedListProps) => any +// override because typescript is stupid From c5cbf9c1106d4dfe198734fc17db483750050f12 Mon Sep 17 00:00:00 2001 From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com> Date: Wed, 28 Jun 2023 13:36:15 +0200 Subject: [PATCH 05/20] Fix FormattedListParts component type --- src/components/listsParts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/listsParts.ts b/src/components/listsParts.ts index 72e4221..c3aaf84 100644 --- a/src/components/listsParts.ts +++ b/src/components/listsParts.ts @@ -54,4 +54,4 @@ export const FormattedListParts = defineComponent( }, }, }, -) +) as (props: FormattedListPartsProps) => any From c6acb75b565e137dd9fd0f2408653f74c444213a Mon Sep 17 00:00:00 2001 From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com> Date: Wed, 28 Jun 2023 13:36:52 +0200 Subject: [PATCH 06/20] Change default slot fallback of FormattedListParts --- src/components/listsParts.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/listsParts.ts b/src/components/listsParts.ts index c3aaf84..2643a62 100644 --- a/src/components/listsParts.ts +++ b/src/components/listsParts.ts @@ -6,6 +6,7 @@ import { type SlotsType, computed, type PropType, + toDisplayString, } from 'vue' import { useVIntl } from '../runtime/index.ts' import { normalizeAttrs } from './utils/index.ts' @@ -38,9 +39,12 @@ export const FormattedListParts = defineComponent( return () => { const { items } = props as FormattedListPartsDefinedProps - const parts = vintl.intl.formatListToParts(items, options.value) as any + const parts = vintl.intl.formatListToParts(items, options.value) - return ctx.slots.default?.({ parts }) ?? parts + return ( + ctx.slots.default?.({ parts }) ?? + parts.map((part) => toDisplayString(part)) + ) } }, { From a570d9607d625893d969b114e3b5dd2c6f16e4b9 Mon Sep 17 00:00:00 2001 From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com> Date: Wed, 28 Jun 2023 15:33:25 +0200 Subject: [PATCH 07/20] Allow non-singular units in FormattedRelativeTime --- src/components/relativeTime.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/relativeTime.ts b/src/components/relativeTime.ts index a48f5a6..7d800d4 100644 --- a/src/components/relativeTime.ts +++ b/src/components/relativeTime.ts @@ -12,7 +12,7 @@ import { normalizeAttrs } from './utils/index.ts' interface RealProps { value: number - unit: Intl.RelativeTimeFormatUnitSingular + unit: Intl.RelativeTimeFormatUnit } export interface FormattedRelativeTimeProps @@ -53,9 +53,9 @@ export const FormattedRelativeTime = defineComponent( inheritAttrs: false, props: { unit: { - type: String as PropType, + type: String as PropType, required: false, - default: 'second', + default: 'seconds', }, value: { type: Number, From 451499bd7420b60e8d2fa9f56a32a7a998aa1c5c Mon Sep 17 00:00:00 2001 From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com> Date: Wed, 28 Jun 2023 17:22:06 +0200 Subject: [PATCH 08/20] Improve FormattedMessage types --- src/components/message.ts | 48 +++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/src/components/message.ts b/src/components/message.ts index a0e0313..d0190fc 100644 --- a/src/components/message.ts +++ b/src/components/message.ts @@ -1,34 +1,42 @@ import { computed, defineComponent, - type VNode, type SlotsType, type SetupContext, } from 'vue' +import type { FormatXMLElementFn, PrimitiveType } from 'intl-messageformat' import { useVIntl } from '../runtime/index.ts' -import { type MessageContent, type MessageValues } from '../types/index.ts' +import { type MessageContent, type MessageValueType } from '../types/index.ts' import { createRecord, normalizeDynamicOutput } from './utils/index.ts' function isValueSlotName(slotName: string): slotName is `~${string}` { return slotName.startsWith('~') } -interface FormattedMessageProps { +type ValuesRecord = Record< + string, + PrimitiveType | T | FormatXMLElementFn +> + +export interface FormattedMessageProps { id: string description?: string | object defaultMessage?: MessageContent - values?: Values + values?: ValuesRecord } -interface FormattedMessageSlots { - [key: string]: (ctx: { children: (VNode | string)[]; values: Values }) => any - [key: `~${string}`]: (ctx: { values: Values }) => any +export interface FormattedMessageSlots { + [key: string]: (ctx: { + children: (T | string)[] + values: ValuesRecord + }) => string | T | (string | T)[] + [key: `~${string}`]: (ctx: { values: ValuesRecord }) => string | T } export const FormattedMessage = defineComponent( - function FormattedMessage( - props: FormattedMessageProps, - ctx: SetupContext<{}, SlotsType>>>, + function FormattedMessage( + props: FormattedMessageProps, + ctx: SetupContext<{}, SlotsType>>>, ) { const descriptor = computed(() => ({ id: props.id, @@ -36,11 +44,11 @@ export const FormattedMessage = defineComponent( description: props.description, })) - const values = computed(() => { - const combinedValues = createRecord() as Values + const values = computed>(() => { + const combinedValues = createRecord() as ValuesRecord Object.assign(combinedValues, props.values) - const slotValues = createRecord() as MessageValues + const slotValues = createRecord() as ValuesRecord const { slots } = ctx @@ -52,8 +60,11 @@ export const FormattedMessage = defineComponent( values: combinedValues, }) } else { - slotValues[slotKey] = (children: (string | VNode)[]) => - slots[slotKey]!({ values: combinedValues, children }) + slotValues[slotKey] = (children: (T | string)[]) => + slots[slotKey]!({ + values: combinedValues, + children, + }) } } @@ -65,7 +76,10 @@ export const FormattedMessage = defineComponent( const vintl = useVIntl() return () => { - const output = vintl.intl.formatMessage(descriptor.value, values.value) + const output = vintl.intl.formatMessage( + descriptor.value, + values.value as any, + ) return normalizeDynamicOutput(output) } @@ -97,4 +111,4 @@ export const FormattedMessage = defineComponent( }, slots: Object as any, }, -) +) as (props: FormattedMessageProps) => any From 98c639f6e2e020b0ea405b63a52f3e89b69ce8ff Mon Sep 17 00:00:00 2001 From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com> Date: Thu, 29 Jun 2023 09:37:25 +0200 Subject: [PATCH 09/20] Revamp normalization by FormattedMessage --- src/components/message.ts | 69 +++++++++++++++++++++++++++++++++------ 1 file changed, 59 insertions(+), 10 deletions(-) diff --git a/src/components/message.ts b/src/components/message.ts index d0190fc..50ec2f1 100644 --- a/src/components/message.ts +++ b/src/components/message.ts @@ -3,11 +3,12 @@ import { defineComponent, type SlotsType, type SetupContext, + type VNodeArrayChildren, } from 'vue' import type { FormatXMLElementFn, PrimitiveType } from 'intl-messageformat' import { useVIntl } from '../runtime/index.ts' import { type MessageContent, type MessageValueType } from '../types/index.ts' -import { createRecord, normalizeDynamicOutput } from './utils/index.ts' +import { createRecord } from './utils/index.ts' function isValueSlotName(slotName: string): slotName is `~${string}` { return slotName.startsWith('~') @@ -33,6 +34,48 @@ export interface FormattedMessageSlots { [key: `~${string}`]: (ctx: { values: ValuesRecord }) => string | T } +class SlotOutput { + constructor(public readonly value: T | T[]) {} +} + +type NonArray = T extends any[] ? never : T +type VNodeChildAtom = NonArray +type VNodeArrayChildrenWith = ( + | T + | VNodeChildAtom + | VNodeArrayChildrenWith +)[] + +function normalizeOutput( + rawOutput: + | string + | SlotOutput + | T + | (string | SlotOutput | T)[], +): VNodeArrayChildrenWith { + if (Array.isArray(rawOutput)) { + const output: VNodeArrayChildrenWith = [] + for (const child of rawOutput) { + if (child instanceof SlotOutput) { + if (Array.isArray(child.value)) { + output.push(...child.value) + } else { + output.push(child.value) + } + } else { + output.push(child) + } + } + return output + } else if (rawOutput instanceof SlotOutput) { + return Array.isArray(rawOutput.value) ? rawOutput.value : [rawOutput.value] + } else if (typeof rawOutput === 'string') { + return [rawOutput] + } + + return [rawOutput] +} + export const FormattedMessage = defineComponent( function FormattedMessage( props: FormattedMessageProps, @@ -48,7 +91,9 @@ export const FormattedMessage = defineComponent( const combinedValues = createRecord() as ValuesRecord Object.assign(combinedValues, props.values) - const slotValues = createRecord() as ValuesRecord + const slotValues = createRecord() as ValuesRecord< + T | SlotOutput | ((children: (T | string)[]) => SlotOutput) + > const { slots } = ctx @@ -56,15 +101,19 @@ export const FormattedMessage = defineComponent( if (slots[slotKey] == null) continue if (isValueSlotName(slotKey)) { - slotValues[slotKey.slice(1)] = slots[slotKey]!({ - values: combinedValues, - }) - } else { - slotValues[slotKey] = (children: (T | string)[]) => + slotValues[slotKey.slice(1)] = new SlotOutput( slots[slotKey]!({ values: combinedValues, - children, - }) + }), + ) + } else { + slotValues[slotKey] = (children: (T | string)[]) => + new SlotOutput( + slots[slotKey]!({ + values: combinedValues, + children, + }), + ) } } @@ -81,7 +130,7 @@ export const FormattedMessage = defineComponent( values.value as any, ) - return normalizeDynamicOutput(output) + return normalizeOutput(output) } }, { From 1d4979e7040e32b6d2c779395a4e1d58dc07e0ca Mon Sep 17 00:00:00 2001 From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com> Date: Thu, 29 Jun 2023 09:43:33 +0200 Subject: [PATCH 10/20] Simply FormattedList children normalisation (by removing all normalisation and let Vue handle that) --- src/components/lists.ts | 6 ++---- src/components/utils/index.ts | 14 -------------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/src/components/lists.ts b/src/components/lists.ts index fa68e1d..3d64c79 100644 --- a/src/components/lists.ts +++ b/src/components/lists.ts @@ -8,7 +8,7 @@ import { type VNode, } from 'vue' import { useVIntl } from '../runtime/index.ts' -import { normalizeAttrs, normalizeDynamicOutput } from './utils/index.ts' +import { normalizeAttrs } from './utils/index.ts' interface FormattedListDefinedProps { items: readonly Item[] @@ -41,9 +41,7 @@ export const FormattedList = defineComponent( const children = vintl.intl.formatList(items, options.value) as any - return ( - ctx.slots.default?.({ children }) ?? normalizeDynamicOutput(children) - ) + return ctx.slots.default?.({ children }) ?? children } }, { diff --git a/src/components/utils/index.ts b/src/components/utils/index.ts index 185575a..5a02f98 100644 --- a/src/components/utils/index.ts +++ b/src/components/utils/index.ts @@ -1,22 +1,8 @@ -import { isVNode, createTextVNode, type VNode } from 'vue' -import type { MessageValueType } from '../../types/index.ts' import { camelize } from '../../utils/strings.ts' export * from './simpleDefiner.ts' export * from './partsDefiner.ts' -export function vnodeOrText(value: T | string) { - return isVNode(value) ? value : createTextVNode(value as any) -} - -export function normalizeDynamicOutput( - value: T | string | (string | T)[], -) { - return ( - Array.isArray(value) ? value.map(vnodeOrText) : [vnodeOrText(value)] - ) satisfies VNode[] -} - export function normalizeAttrs(attrs: Record) { const normalizedAttrs: Record = Object.create(null) for (const key in attrs) normalizedAttrs[camelize(key)] = attrs[key] From b5b8c410447584c82690f57c7270b54190911506 Mon Sep 17 00:00:00 2001 From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com> Date: Thu, 29 Jun 2023 09:54:01 +0200 Subject: [PATCH 11/20] Make FormattedPlural fall back to other slot if rule slot is not defined --- src/components/plural.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/plural.ts b/src/components/plural.ts index efb20c2..ce880b7 100644 --- a/src/components/plural.ts +++ b/src/components/plural.ts @@ -43,7 +43,9 @@ export const FormattedPlural = defineComponent( const rule = vintl.intl.formatPlural(value, $options.value) - const ruleRender = ctx.slots[rule]?.() ?? [] + const ruleSlot = ctx.slots[rule] ?? ctx.slots.other + + const ruleRender = ruleSlot != null ? ruleSlot() : [] return ctx.slots.default?.({ children: ruleRender }) ?? ruleRender } From 365733a14c6b283b67c0c76da059431514d4bd14 Mon Sep 17 00:00:00 2001 From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com> Date: Thu, 29 Jun 2023 09:54:22 +0200 Subject: [PATCH 12/20] Make FormattedPluralSlots generic optional --- src/components/plural.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/plural.ts b/src/components/plural.ts index ce880b7..b7dc695 100644 --- a/src/components/plural.ts +++ b/src/components/plural.ts @@ -23,7 +23,7 @@ export interface FormattedPluralProps extends FormattedPluralDefinedProps, FormatPluralOptions {} -export interface FormattedPluralSlots extends PluralSlots { +export interface FormattedPluralSlots extends PluralSlots { default(props: { children: string | T | (string | T)[] }): any } From ff909dc29a38026cfe5864e40e1783a521d71371 Mon Sep 17 00:00:00 2001 From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com> Date: Thu, 29 Jun 2023 09:55:20 +0200 Subject: [PATCH 13/20] Pass value prop to FormattedPlural slots --- src/components/plural.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/plural.ts b/src/components/plural.ts index b7dc695..e987e18 100644 --- a/src/components/plural.ts +++ b/src/components/plural.ts @@ -12,7 +12,7 @@ import { normalizeAttrs } from './utils/index.ts' type PluralSelectors = ReturnType type PluralSlots = { - [K in PluralSelectors]: () => any + [K in PluralSelectors]: (props: { value: number }) => any } interface FormattedPluralDefinedProps { @@ -45,7 +45,7 @@ export const FormattedPlural = defineComponent( const ruleSlot = ctx.slots[rule] ?? ctx.slots.other - const ruleRender = ruleSlot != null ? ruleSlot() : [] + const ruleRender = ruleSlot != null ? ruleSlot({ value }) : [] return ctx.slots.default?.({ children: ruleRender }) ?? ruleRender } From 3f7b620f4c20a697c5ca0e2a0d4248accb9682ee Mon Sep 17 00:00:00 2001 From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com> Date: Thu, 29 Jun 2023 09:55:37 +0200 Subject: [PATCH 14/20] Make all slots of FormattedPlural optional --- src/components/plural.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/plural.ts b/src/components/plural.ts index e987e18..6484fbd 100644 --- a/src/components/plural.ts +++ b/src/components/plural.ts @@ -12,7 +12,7 @@ import { normalizeAttrs } from './utils/index.ts' type PluralSelectors = ReturnType type PluralSlots = { - [K in PluralSelectors]: (props: { value: number }) => any + [K in PluralSelectors]?: (props: { value: number }) => any } interface FormattedPluralDefinedProps { @@ -24,7 +24,7 @@ export interface FormattedPluralProps FormatPluralOptions {} export interface FormattedPluralSlots extends PluralSlots { - default(props: { children: string | T | (string | T)[] }): any + default?(props: { children: string | T | (string | T)[] }): any } export const FormattedPlural = defineComponent( From 8b7f7b829d7ab38ab0bbb38445c5889b8b6993b1 Mon Sep 17 00:00:00 2001 From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com> Date: Thu, 29 Jun 2023 10:31:52 +0200 Subject: [PATCH 15/20] Fix FormattedPlural prop/attr handling --- src/components/plural.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/components/plural.ts b/src/components/plural.ts index 6484fbd..1db5c88 100644 --- a/src/components/plural.ts +++ b/src/components/plural.ts @@ -51,19 +51,13 @@ export const FormattedPlural = defineComponent( } }, { + inheritAttrs: false, props: { value: { type: Number, required: false, default: 0, }, - options: { - type: Object, - required: false, - default() { - return {} - }, - }, }, }, -) +) as (props: FormattedPluralProps) => any From d9bba71169ba6af98774664a1b0a7245aee6c359 Mon Sep 17 00:00:00 2001 From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com> Date: Thu, 29 Jun 2023 10:32:22 +0200 Subject: [PATCH 16/20] Make generic of FormattedPlural optional --- src/components/plural.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/plural.ts b/src/components/plural.ts index 1db5c88..53a0804 100644 --- a/src/components/plural.ts +++ b/src/components/plural.ts @@ -5,7 +5,6 @@ import { type SetupContext, type SlotsType, } from 'vue' -import { type MessageValueType } from '../types/index.ts' import { useVIntl } from '../runtime/index.ts' import { normalizeAttrs } from './utils/index.ts' @@ -28,7 +27,7 @@ export interface FormattedPluralSlots extends PluralSlots { } export const FormattedPlural = defineComponent( - ( + ( props: FormattedPluralProps, ctx: SetupContext<{}, SlotsType>>>, ) => { From 7fabcf32512a60931c3cb31dc89a29df98eac898 Mon Sep 17 00:00:00 2001 From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com> Date: Thu, 29 Jun 2023 13:27:23 +0200 Subject: [PATCH 17/20] In FormattedListParts, return part values as is if no default slot exists --- src/components/listsParts.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/listsParts.ts b/src/components/listsParts.ts index 2643a62..de78173 100644 --- a/src/components/listsParts.ts +++ b/src/components/listsParts.ts @@ -6,7 +6,6 @@ import { type SlotsType, computed, type PropType, - toDisplayString, } from 'vue' import { useVIntl } from '../runtime/index.ts' import { normalizeAttrs } from './utils/index.ts' @@ -41,10 +40,11 @@ export const FormattedListParts = defineComponent( const parts = vintl.intl.formatListToParts(items, options.value) - return ( - ctx.slots.default?.({ parts }) ?? - parts.map((part) => toDisplayString(part)) - ) + const defaultSlot = ctx.slots.default + + return defaultSlot == null + ? parts.map((part) => part.value) + : defaultSlot({ parts }) } }, { From 945ac406bcfb2ccec472eee487ba3038efdadacd Mon Sep 17 00:00:00 2001 From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com> Date: Mon, 3 Jul 2023 16:49:23 +0200 Subject: [PATCH 18/20] Add tests for all unique components --- .eslintrc.json | 2 +- package.json | 1 + pnpm-lock.yaml | 315 ++++++++++++++++-- test/components/FormattedList/index.test.ts | 59 ++++ test/components/FormattedList/listDisplay.tsx | 65 ++++ .../FormattedListParts/index.test.ts | 62 ++++ .../FormattedListParts/listPartsDisplay.tsx | 79 +++++ .../components/FormattedMessage/index.test.ts | 65 ++++ .../FormattedMessage/messageDisplay.tsx | 89 +++++ test/components/FormattedNumber/counter.tsx | 54 +++ test/components/FormattedNumber/index.test.ts | 70 ++++ .../FormattedNumberParts/counter.tsx | 43 +++ .../FormattedNumberParts/index.test.ts | 58 ++++ test/components/FormattedPlural/index.test.ts | 97 ++++++ .../FormattedPlural/pluralDisplay.tsx | 66 ++++ .../FormattedRelativeTime/index.test.ts | 67 ++++ .../relativeTimeDisplay.tsx | 55 +++ test/utils/index.ts | 33 ++ tsconfig.tests.json | 15 +- vitest.config.ts | 4 + 20 files changed, 1275 insertions(+), 24 deletions(-) create mode 100644 test/components/FormattedList/index.test.ts create mode 100644 test/components/FormattedList/listDisplay.tsx create mode 100644 test/components/FormattedListParts/index.test.ts create mode 100644 test/components/FormattedListParts/listPartsDisplay.tsx create mode 100644 test/components/FormattedMessage/index.test.ts create mode 100644 test/components/FormattedMessage/messageDisplay.tsx create mode 100644 test/components/FormattedNumber/counter.tsx create mode 100644 test/components/FormattedNumber/index.test.ts create mode 100644 test/components/FormattedNumberParts/counter.tsx create mode 100644 test/components/FormattedNumberParts/index.test.ts create mode 100644 test/components/FormattedPlural/index.test.ts create mode 100644 test/components/FormattedPlural/pluralDisplay.tsx create mode 100644 test/components/FormattedRelativeTime/index.test.ts create mode 100644 test/components/FormattedRelativeTime/relativeTimeDisplay.tsx create mode 100644 test/utils/index.ts diff --git a/.eslintrc.json b/.eslintrc.json index 72f950f..180a629 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -41,7 +41,7 @@ "parserOptions": { "project": "./tsconfig.build.json" } }, { - "files": ["./vitest.config.ts", "./test/*.test.ts"], + "files": ["./vitest.config.ts", "./test/**/*.ts", "./test/**/*.tsx"], "parserOptions": { "project": "./tsconfig.tests.json" } } ] diff --git a/package.json b/package.json index 99ffce8..b8593db 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "devDependencies": { "@changesets/cli": "^2.26.1", "@nuxtjs/eslint-config-typescript": "^12.0.0", + "@testing-library/vue": "^7.0.0", "@types/node": "^18.16.16", "del-cli": "^5.0.0", "eslint": "^8.42.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1fada30..b725380 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,6 +34,9 @@ devDependencies: '@nuxtjs/eslint-config-typescript': specifier: ^12.0.0 version: 12.0.0(eslint@8.42.0)(typescript@5.1.3) + '@testing-library/vue': + specifier: ^7.0.0 + version: 7.0.0(@vue/compiler-sfc@3.3.4)(vue@3.3.4) '@types/node': specifier: ^18.16.16 version: 18.16.16 @@ -1193,6 +1196,38 @@ packages: rollup: 3.20.6 dev: true + /@testing-library/dom@9.3.0: + resolution: {integrity: sha512-Dffe68pGwI6WlLRYR2I0piIkyole9cSBH5jGQKCGMRpHW5RHCqAUaqc2Kv0tUyd4dU4DLPKhJIjyKOnjv4tuUw==} + engines: {node: '>=14'} + dependencies: + '@babel/code-frame': 7.21.4 + '@babel/runtime': 7.22.3 + '@types/aria-query': 5.0.1 + aria-query: 5.1.3 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + dev: true + + /@testing-library/vue@7.0.0(@vue/compiler-sfc@3.3.4)(vue@3.3.4): + resolution: {integrity: sha512-JU/q93HGo2qdm1dCgWymkeQlfpC0/0/DBZ2nAHgEAsVZxX11xVIxT7gbXdI7HACQpUbsUWt1zABGU075Fzt9XQ==} + engines: {node: '>=14'} + peerDependencies: + '@vue/compiler-sfc': '>= 3' + vue: '>= 3' + dependencies: + '@babel/runtime': 7.22.3 + '@testing-library/dom': 9.3.0 + '@vue/compiler-sfc': 3.3.4 + '@vue/test-utils': 2.3.2(vue@3.3.4) + vue: 3.3.4 + dev: true + + /@types/aria-query@5.0.1: + resolution: {integrity: sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==} + dev: true + /@types/chai-subset@1.3.3: resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} dependencies: @@ -1529,6 +1564,18 @@ packages: /@vue/shared@3.3.4: resolution: {integrity: sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==} + /@vue/test-utils@2.3.2(vue@3.3.4): + resolution: {integrity: sha512-hJnVaYhbrIm0yBS0+e1Y0Sj85cMyAi+PAbK4JHqMRUZ6S622Goa+G7QzkRSyvCteG8wop7tipuEbHoZo26wsSA==} + peerDependencies: + vue: ^3.0.1 + dependencies: + js-beautify: 1.14.6 + vue: 3.3.4 + optionalDependencies: + '@vue/compiler-dom': 3.3.4 + '@vue/server-renderer': 3.3.4(vue@3.3.4) + dev: true + /@vueuse/core@10.1.2(vue@3.3.4): resolution: {integrity: sha512-roNn8WuerI56A5uiTyF/TEYX0Y+VKlhZAF94unUfdhbDUI+NfwQMn4FUnUscIRUhv3344qvAghopU4bzLPNFlA==} dependencies: @@ -1604,6 +1651,10 @@ packages: - vue dev: true + /abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + dev: true + /acorn-jsx@5.3.2(acorn@8.8.2): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1702,14 +1753,27 @@ packages: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} dev: true + /aria-query@5.1.3: + resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} + dependencies: + deep-equal: 2.2.1 + dev: true + + /array-buffer-byte-length@1.0.0: + resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + dependencies: + call-bind: 1.0.2 + is-array-buffer: 3.0.2 + dev: true + /array-includes@3.1.6: resolution: {integrity: sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==} engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 + define-properties: 1.2.0 es-abstract: 1.20.5 - get-intrinsic: 1.1.3 + get-intrinsic: 1.2.1 is-string: 1.0.7 dev: true @@ -1723,7 +1787,7 @@ packages: engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 + define-properties: 1.2.0 es-abstract: 1.20.5 es-shim-unscopables: 1.0.0 dev: true @@ -1737,6 +1801,11 @@ packages: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} dev: true + /available-typed-arrays@1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + dev: true + /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true @@ -1821,7 +1890,7 @@ packages: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} dependencies: function-bind: 1.1.1 - get-intrinsic: 1.1.3 + get-intrinsic: 1.2.1 dev: true /callsites@3.1.0: @@ -1971,6 +2040,10 @@ packages: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} dev: true + /commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + dev: true + /comment-parser@1.3.1: resolution: {integrity: sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==} engines: {node: '>= 12.0.0'} @@ -1998,6 +2071,13 @@ packages: well-known-symbols: 2.0.0 dev: true + /config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + dev: true + /consola@3.1.0: resolution: {integrity: sha512-rrrJE6rP0qzl/Srg+C9x/AE5Kxfux7reVm1Wh0wCjuXvih6DqZgqDZe8auTD28fzJ9TF0mHlSDrPpWlujQRo1Q==} dev: true @@ -2130,6 +2210,29 @@ packages: type-detect: 4.0.8 dev: true + /deep-equal@2.2.1: + resolution: {integrity: sha512-lKdkdV6EOGoVn65XaOsPdH4rMxTZOnmFyuIkMjM1i5HHCbfjC97dawgTAy0deYNfuqUqW+Q5VrVaQYtUpSd6yQ==} + dependencies: + array-buffer-byte-length: 1.0.0 + call-bind: 1.0.2 + es-get-iterator: 1.1.3 + get-intrinsic: 1.2.1 + is-arguments: 1.1.1 + is-array-buffer: 3.0.2 + is-date-object: 1.0.5 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + isarray: 2.0.5 + object-is: 1.1.5 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.5.0 + side-channel: 1.0.4 + which-boxed-primitive: 1.0.2 + which-collection: 1.0.1 + which-typed-array: 1.1.9 + dev: true + /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true @@ -2158,6 +2261,14 @@ packages: object-keys: 1.1.1 dev: true + /define-properties@1.2.0: + resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==} + engines: {node: '>= 0.4'} + dependencies: + has-property-descriptors: 1.0.0 + object-keys: 1.1.1 + dev: true + /defu@6.1.2: resolution: {integrity: sha512-+uO4+qr7msjNNWKYPHqN/3+Dx3NFkmIzayk2L1MyZQlvgZb/J1A0fo410dpKrN2SnqFjt8n4JL8fDJE0wIgjFQ==} dev: true @@ -2221,6 +2332,20 @@ packages: esutils: 2.0.3 dev: true + /dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dev: true + + /editorconfig@0.15.3: + resolution: {integrity: sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==} + hasBin: true + dependencies: + commander: 2.20.3 + lru-cache: 4.1.5 + semver: 5.7.1 + sigmund: 1.0.1 + dev: true + /electron-to-chromium@1.4.368: resolution: {integrity: sha512-e2aeCAixCj9M7nJxdB/wDjO6mbYX+lJJxSJCXDzlr5YPGYVofuJwGN9nKg2o6wWInjX6XmxRinn3AeJMK81ltw==} dev: true @@ -2263,7 +2388,7 @@ packages: es-to-primitive: 1.2.1 function-bind: 1.1.1 function.prototype.name: 1.1.5 - get-intrinsic: 1.1.3 + get-intrinsic: 1.2.1 get-symbol-description: 1.0.0 gopd: 1.0.1 has: 1.0.3 @@ -2279,13 +2404,27 @@ packages: object-inspect: 1.12.2 object-keys: 1.1.1 object.assign: 4.1.4 - regexp.prototype.flags: 1.4.3 + regexp.prototype.flags: 1.5.0 safe-regex-test: 1.0.0 string.prototype.trimend: 1.0.6 string.prototype.trimstart: 1.0.6 unbox-primitive: 1.0.2 dev: true + /es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + has-symbols: 1.0.3 + is-arguments: 1.1.1 + is-map: 2.0.2 + is-set: 2.0.2 + is-string: 1.0.7 + isarray: 2.0.5 + stop-iteration-iterator: 1.0.0 + dev: true + /es-shim-unscopables@1.0.0: resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} dependencies: @@ -2817,6 +2956,12 @@ packages: tabbable: 6.1.2 dev: true + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: true + /fs-extra@11.1.1: resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} engines: {node: '>=14.14'} @@ -2865,7 +3010,7 @@ packages: engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 + define-properties: 1.2.0 es-abstract: 1.20.5 functions-have-names: 1.2.3 dev: true @@ -2888,11 +3033,12 @@ packages: resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} dev: true - /get-intrinsic@1.1.3: - resolution: {integrity: sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==} + /get-intrinsic@1.2.1: + resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} dependencies: function-bind: 1.1.1 has: 1.0.3 + has-proto: 1.0.1 has-symbols: 1.0.3 dev: true @@ -2901,7 +3047,7 @@ packages: engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 - get-intrinsic: 1.1.3 + get-intrinsic: 1.2.1 dev: true /get-tsconfig@4.2.0: @@ -2990,7 +3136,7 @@ packages: /gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: - get-intrinsic: 1.1.3 + get-intrinsic: 1.2.1 dev: true /graceful-fs@4.2.11: @@ -3038,7 +3184,12 @@ packages: /has-property-descriptors@1.0.0: resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} dependencies: - get-intrinsic: 1.1.3 + get-intrinsic: 1.2.1 + dev: true + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} dev: true /has-symbols@1.0.3: @@ -3132,11 +3283,15 @@ packages: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} dev: true + /ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + dev: true + /internal-slot@1.0.4: resolution: {integrity: sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==} engines: {node: '>= 0.4'} dependencies: - get-intrinsic: 1.1.3 + get-intrinsic: 1.2.1 has: 1.0.3 side-channel: 1.0.4 dev: true @@ -3150,6 +3305,22 @@ packages: tslib: 2.4.1 dev: false + /is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: true + + /is-array-buffer@3.0.2: + resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-typed-array: 1.1.10 + dev: true + /is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} dev: true @@ -3230,6 +3401,10 @@ packages: is-extglob: 2.1.1 dev: true + /is-map@2.0.2: + resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==} + dev: true + /is-module@1.0.0: resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} dev: true @@ -3285,6 +3460,10 @@ packages: has-tostringtag: 1.0.0 dev: true + /is-set@2.0.2: + resolution: {integrity: sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==} + dev: true + /is-shared-array-buffer@1.0.2: resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} dependencies: @@ -3312,12 +3491,34 @@ packages: has-symbols: 1.0.3 dev: true + /is-typed-array@1.1.10: + resolution: {integrity: sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + dev: true + + /is-weakmap@2.0.1: + resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==} + dev: true + /is-weakref@1.0.2: resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} dependencies: call-bind: 1.0.2 dev: true + /is-weakset@2.0.2: + resolution: {integrity: sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + dev: true + /is-windows@1.0.2: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} @@ -3330,6 +3531,10 @@ packages: is-docker: 2.2.1 dev: true + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + dev: true + /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true @@ -3339,6 +3544,17 @@ packages: hasBin: true dev: true + /js-beautify@1.14.6: + resolution: {integrity: sha512-GfofQY5zDp+cuHc+gsEXKPpNw2KbPddreEo35O6jT6i0RVK6LhsoYBhq5TvK4/n74wnA0QbK8gGd+jUZwTMKJw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + config-chain: 1.1.13 + editorconfig: 0.15.3 + glob: 8.1.0 + nopt: 6.0.0 + dev: true + /js-string-escape@1.0.1: resolution: {integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==} engines: {node: '>= 0.8'} @@ -3506,6 +3722,11 @@ packages: yallist: 4.0.0 dev: true + /lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + dev: true + /magic-string@0.27.0: resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} engines: {node: '>=12'} @@ -3889,6 +4110,14 @@ packages: resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==} dev: true + /nopt@6.0.0: + resolution: {integrity: sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + hasBin: true + dependencies: + abbrev: 1.1.1 + dev: true + /normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: @@ -3918,6 +4147,14 @@ packages: resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==} dev: true + /object-is@1.1.5: + resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + dev: true + /object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} @@ -3938,7 +4175,7 @@ packages: engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 + define-properties: 1.2.0 es-abstract: 1.20.5 dev: true @@ -4188,6 +4425,10 @@ packages: react-is: 17.0.2 dev: true + /proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + dev: true + /pseudomap@1.0.2: resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} dev: true @@ -4288,12 +4529,12 @@ packages: hasBin: true dev: true - /regexp.prototype.flags@1.4.3: - resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==} + /regexp.prototype.flags@1.5.0: + resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==} engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 + define-properties: 1.2.0 functions-have-names: 1.2.3 dev: true @@ -4389,7 +4630,7 @@ packages: resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} dependencies: call-bind: 1.0.2 - get-intrinsic: 1.1.3 + get-intrinsic: 1.2.1 is-regex: 1.1.4 dev: true @@ -4466,7 +4707,7 @@ packages: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} dependencies: call-bind: 1.0.2 - get-intrinsic: 1.1.3 + get-intrinsic: 1.2.1 object-inspect: 1.12.2 dev: true @@ -4474,6 +4715,10 @@ packages: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} dev: true + /sigmund@1.0.1: + resolution: {integrity: sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==} + dev: true + /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} dev: true @@ -4546,6 +4791,13 @@ packages: resolution: {integrity: sha512-uUZI65yrV2Qva5gqE0+A7uVAvO40iPo6jGhs7s8keRfHCmtg+uB2X6EiLGCI9IgL1J17xGhvoOqSz79lzICPTA==} dev: true + /stop-iteration-iterator@1.0.0: + resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} + engines: {node: '>= 0.4'} + dependencies: + internal-slot: 1.0.4 + dev: true + /stream-transform@2.1.3: resolution: {integrity: sha512-9GHUiM5hMiCi6Y03jD2ARC1ettBXkQBoQAe7nJsPknnI0ow10aXjTnew8QtYQmLjzn974BnmWEAJgCY6ZP1DeQ==} dependencies: @@ -4565,7 +4817,7 @@ packages: resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==} dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 + define-properties: 1.2.0 es-abstract: 1.20.5 dev: true @@ -4573,7 +4825,7 @@ packages: resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==} dependencies: call-bind: 1.0.2 - define-properties: 1.1.4 + define-properties: 1.2.0 es-abstract: 1.20.5 dev: true @@ -5162,6 +5414,15 @@ packages: is-symbol: 1.0.4 dev: true + /which-collection@1.0.1: + resolution: {integrity: sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==} + dependencies: + is-map: 2.0.2 + is-set: 2.0.2 + is-weakmap: 2.0.1 + is-weakset: 2.0.2 + dev: true + /which-module@2.0.1: resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} dev: true @@ -5174,6 +5435,18 @@ packages: path-exists: 4.0.0 dev: true + /which-typed-array@1.1.9: + resolution: {integrity: sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + is-typed-array: 1.1.10 + dev: true + /which@1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} hasBin: true diff --git a/test/components/FormattedList/index.test.ts b/test/components/FormattedList/index.test.ts new file mode 100644 index 0000000..55f4986 --- /dev/null +++ b/test/components/FormattedList/index.test.ts @@ -0,0 +1,59 @@ +import { afterAll, beforeEach, afterEach, describe, expect, it } from 'vitest' +import { cleanup, fireEvent, render } from '@testing-library/vue' +import { + createVIntlPlugin, + withAbnormalSpacesReplaced, +} from '../../utils/index.ts' +import { ListDisplay } from './listDisplay.tsx' + +describe('FormattedList', () => { + afterAll(() => cleanup()) + + const vintl = createVIntlPlugin(['en-US', 'uk']) + const { plugin, controller, resetController } = vintl + + const { getByText, getByTestId } = render(ListDisplay, { + global: { plugins: [plugin] }, + }) + + let list: HTMLElement + + const refreshList = () => (list = getByTestId('list-display')) + + beforeEach(async () => { + await fireEvent.click(getByText('Reset')) + refreshList() + }) + + afterEach(resetController) + + it('renders', async () => { + expect(list.textContent).toMatchInlineSnapshot('"1, 2, or 3"') + + await fireEvent.click(getByText('Add item')) + expect(list.textContent).toMatchInlineSnapshot('"1, 2, 3, or 4"') + }) + + it('changes locale', async () => { + await controller.changeLocale('uk') + const content = withAbnormalSpacesReplaced(list.textContent!) + expect(content).toMatchInlineSnapshot('"1, 2 або 3"') + }) + + it('renders as a slot', async () => { + await fireEvent.click(getByText('Slots on')) + const content = refreshList().textContent + expect(content).toMatchInlineSnapshot('"List is: 1, 2, or 3"') + + const slot = getByTestId('list-slot') + expect(slot.textContent!).toMatchInlineSnapshot('"1, 2, or 3"') + }) + + it('renders JSX items', async () => { + await fireEvent.click(getByText('JSX on')) + + expect(list.querySelector('b')).toBeDefined() + + await fireEvent.click(getByText('Press me')) + }) +}) diff --git a/test/components/FormattedList/listDisplay.tsx b/test/components/FormattedList/listDisplay.tsx new file mode 100644 index 0000000..f16d41d --- /dev/null +++ b/test/components/FormattedList/listDisplay.tsx @@ -0,0 +1,65 @@ +import { computed, defineComponent, ref } from 'vue' +import { + FormattedList, + type FormattedListSlots, +} from '../../../dist/components' + +export const ListDisplay = defineComponent(() => { + const list = ref(['1', '2', '3']) + + let increment = 3 + + const addListItem = () => list.value.push(String(++increment)) + + const useSlots = ref(false) + const enableSlots = () => (useSlots.value = true) + + const useJSXNodes = ref(false) + const enableJSXNodes = () => (useJSXNodes.value = true) + + const listToRender = computed(() => { + return useJSXNodes.value + ? [...list.value, Bold, ] + : list.value + }) + + const reset = () => { + list.value = ['1', '2', '3'] + increment = 3 + useSlots.value = false + useJSXNodes.value = false + } + + return () => { + let display: JSX.Element + + if (useSlots.value) { + const slots: FormattedListSlots = { + default: ({ children }) => ( + <> + {'List is: '} + {children} + + ), + } + + display = ( + + {slots} + + ) + } else { + display = + } + + return ( + <> +

{display}

+ + + + + + ) + } +}) diff --git a/test/components/FormattedListParts/index.test.ts b/test/components/FormattedListParts/index.test.ts new file mode 100644 index 0000000..310c58e --- /dev/null +++ b/test/components/FormattedListParts/index.test.ts @@ -0,0 +1,62 @@ +import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest' +import { cleanup, fireEvent, render } from '@testing-library/vue' +import { + createVIntlPlugin, + withAbnormalSpacesReplaced, +} from '../../utils/index.ts' +import { ListPartsDisplay } from './listPartsDisplay.tsx' + +describe('FormattedListParts', () => { + afterAll(() => cleanup()) + + const vintl = createVIntlPlugin(['en-US', 'uk']) + const { plugin, controller, resetController } = vintl + + const { getByText, getByTestId } = render(ListPartsDisplay, { + global: { plugins: [plugin] }, + }) + + let list: HTMLElement + + const refreshList = () => (list = getByTestId('list-display')) + + beforeEach(async () => { + await fireEvent.click(getByText('Reset')) + refreshList() + }) + + afterEach(resetController) + + it('renders', async () => { + expect(list.textContent).toMatchInlineSnapshot('"1, 2, or 3"') + + await fireEvent.click(getByText('Add item')) + expect(list.textContent).toMatchInlineSnapshot('"1, 2, 3, or 4"') + }) + + it('changes locale', async () => { + await controller.changeLocale('uk') + + const content = withAbnormalSpacesReplaced(list.textContent!) + expect(content).toMatchInlineSnapshot('"1, 2 або 3"') + }) + + it('renders as a slot', async () => { + await fireEvent.click(getByText('Slots on')) + + const content = refreshList().textContent + expect(content).toMatchInlineSnapshot('"List is: 1, 2, or 3"') + + const slot = getByTestId('list-slot') + expect(slot.textContent!).toMatchInlineSnapshot('"1, 2, or 3"') + }) + + it('renders JSX items', async () => { + await fireEvent.click(getByText('JSX on')) + + const bold = refreshList().querySelector('b') + expect(bold?.textContent).toMatchInlineSnapshot('"Bold"') + + expect(getByText('Press me')).toBeDefined() + }) +}) diff --git a/test/components/FormattedListParts/listPartsDisplay.tsx b/test/components/FormattedListParts/listPartsDisplay.tsx new file mode 100644 index 0000000..9ba3cd6 --- /dev/null +++ b/test/components/FormattedListParts/listPartsDisplay.tsx @@ -0,0 +1,79 @@ +import { computed, defineComponent, ref } from 'vue' +import { + FormattedListParts, + type FormattedListPartsSlots, +} from '../../../dist/components' + +export const ListPartsDisplay = defineComponent(() => { + const list = ref(['1', '2', '3']) + + let increment = 3 + + const addListItem = () => list.value.push(String(++increment)) + + const useSlots = ref(false) + const enableSlots = () => (useSlots.value = true) + + const useJSXNodes = ref(false) + const enableJSXNodes = () => (useJSXNodes.value = true) + + const listToRender = computed(() => { + return useJSXNodes.value + ? [...list.value, Bold, ] + : list.value + }) + + const reset = () => { + list.value = ['1', '2', '3'] + increment = 3 + useSlots.value = false + useJSXNodes.value = false + } + + return () => { + let display: JSX.Element + + if (useSlots.value) { + const slots: FormattedListPartsSlots = { + default: ({ parts }) => ( + <> + {'List is: '} +
    + {parts.map((part) => ( +
  • {part.value}
  • + ))} +
+ + ), + } + + display = ( + + {slots} + + ) + } else { + display = ( + + ) + } + + return ( + <> +

{display}

+ + + + + + ) + } +}) diff --git a/test/components/FormattedMessage/index.test.ts b/test/components/FormattedMessage/index.test.ts new file mode 100644 index 0000000..a27fdb8 --- /dev/null +++ b/test/components/FormattedMessage/index.test.ts @@ -0,0 +1,65 @@ +import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest' +import { cleanup, fireEvent, render } from '@testing-library/vue' +import { createVIntlPlugin } from '../../utils/index.ts' +import { messagesPayload, MessageDisplay } from './messageDisplay.tsx' + +describe('FormattedMessage', () => { + afterAll(() => cleanup()) + + const vintl = createVIntlPlugin(['en-US', 'uk'], (e) => { + e.addMessages(messagesPayload?.[e.locale.tag] ?? {}) + }) + + const { plugin, controller, resetController } = vintl + + const { getByText, getByTestId } = render(MessageDisplay, { + global: { plugins: [plugin] }, + }) + + let display: HTMLElement + + const refreshDisplay = () => (display = getByTestId('message-display')) + + beforeEach(async () => { + await fireEvent.click(getByText('Reset')) + refreshDisplay() + }) + + afterEach(async () => { + await resetController() + }) + + const content = () => display.textContent + + it('renders', async () => { + expect(content()).toMatchInlineSnapshot( + '"Hello, Oleksandr. You have 1 new message"', + ) + + await fireEvent.click(getByText('+1')) + expect(content()).toMatchInlineSnapshot( + '"Hello, Oleksandr. You have 2 new messages"', + ) + }) + + it('changes locale', async () => { + await controller.changeLocale('uk') + expect(content()).toMatchInlineSnapshot( + '"Привіт, Oleksandr. У вас 1 нове повідомлення"', + ) + + await fireEvent.click(getByText('+1')) + expect(content()).toMatchInlineSnapshot( + '"Привіт, Oleksandr. У вас 2 нових повідомлень"', + ) + }) + + it('renders with slots', async () => { + await fireEvent.click(getByText('Slots on')) + refreshDisplay() + + expect(display.innerHTML).toMatchInlineSnapshot( + '"Hello, Oleksandr 👋 You have 1 new message"', + ) + }) +}) diff --git a/test/components/FormattedMessage/messageDisplay.tsx b/test/components/FormattedMessage/messageDisplay.tsx new file mode 100644 index 0000000..28d5649 --- /dev/null +++ b/test/components/FormattedMessage/messageDisplay.tsx @@ -0,0 +1,89 @@ +import { defineMessages } from '@formatjs/intl' +import { defineComponent, ref } from 'vue' +import { + FormattedMessage, + type FormattedMessageSlots, +} from '../../../dist/components' + +const messages = defineMessages({ + greeting: { + id: 'greeting', + defaultMessage: + 'Hello, {name}. You have {count, plural, one {# new message} other {# new messages}}', + }, + greetingBold: { + id: 'greeting.bold', + defaultMessage: + 'Hello, {name} {wave_emoji} You have {count, plural, one {# new message} other {# new messages}}', + }, +} as const) + +export const messagesPayload: Record< + string, + { + [K in (typeof messages)[keyof typeof messages]['id']]: string + } +> = { + 'en-US': { + greeting: messages.greeting.defaultMessage, + 'greeting.bold': messages.greetingBold.defaultMessage, + }, + uk: { + greeting: + 'Привіт, {name}. У вас {count, plural, one {# нове повідомлення} other {# нових повідомлень}}', + 'greeting.bold': + 'Привіт, {name} {wave_emoji} У вас {count, plural, one {# нове повідомлення} other {# нових повідомлень}}', + }, +} as const + +export const MessageDisplay = defineComponent(() => { + const name = ref('Oleksandr') + + const unreadMessages = ref(1) + const incrementByOne = () => (unreadMessages.value += 1) + + const useSlots = ref(false) + const enableSlots = () => (useSlots.value = true) + + const reset = () => { + name.value = 'Oleksandr' + unreadMessages.value = 1 + useSlots.value = false + } + + return () => { + let display: JSX.Element + + if (useSlots.value) { + const slots: FormattedMessageSlots = { + '~wave_emoji': () => 👋, + bold: ({ children }) => {children}, + } + + display = ( + + {slots} + + ) + } else { + display = ( + + ) + } + + return ( + <> +

{display}

+ + + + + ) + } +}) diff --git a/test/components/FormattedNumber/counter.tsx b/test/components/FormattedNumber/counter.tsx new file mode 100644 index 0000000..21d8733 --- /dev/null +++ b/test/components/FormattedNumber/counter.tsx @@ -0,0 +1,54 @@ +import { defineComponent, ref } from 'vue' +import { + FormattedNumber, + type FormattedNumberSlots, +} from '../../../dist/components' + +export const Counter = defineComponent(() => { + const count = ref(0) + const incrementByOne = () => (count.value += 1) + const incrementByThousand = () => (count.value += 1000) + + const useSlots = ref(false) + const enableSlots = () => { + useSlots.value = true + } + + const reset = () => { + count.value = 0 + useSlots.value = false + } + + return () => { + let display: JSX.Element + + if (useSlots.value) { + const slots: FormattedNumberSlots = { + default: ({ formattedValue }) => ( + <> + {'Count is: '} + {formattedValue} + + ), + } + + display = ( + + {slots} + + ) + } else { + display = + } + + return ( + <> +

{display}

+ + + + + + ) + } +}) diff --git a/test/components/FormattedNumber/index.test.ts b/test/components/FormattedNumber/index.test.ts new file mode 100644 index 0000000..e1b3cf4 --- /dev/null +++ b/test/components/FormattedNumber/index.test.ts @@ -0,0 +1,70 @@ +import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest' +import { cleanup, fireEvent, render } from '@testing-library/vue' +import { + createVIntlPlugin, + withAbnormalSpacesReplaced, +} from '../../utils/index.ts' +import { Counter } from './counter.tsx' + +describe('FormattedNumber', () => { + afterAll(() => cleanup()) + + const vintl = createVIntlPlugin(['en-US', 'uk']) + const { plugin, controller, resetController } = vintl + + const { getByText, getByTestId } = render(Counter, { + global: { plugins: [plugin] }, + }) + + let counter: HTMLElement + const refreshCounter = () => (counter = getByTestId('counter')) + + beforeEach(async () => { + await fireEvent.click(getByText('Reset')) + refreshCounter() + }) + + afterEach(resetController) + + const content = () => withAbnormalSpacesReplaced(counter.textContent!) + + it('renders', async () => { + expect(content()).toMatchInlineSnapshot('"0"') + + await fireEvent.click(getByText('+1')) + expect(content()).toMatchInlineSnapshot('"1"') + + await fireEvent.click(getByText('+1000')) + expect(content()).toMatchInlineSnapshot('"1K"') + }) + + it('changes locale', async () => { + await controller.changeLocale('uk') + + expect(content()).toMatchInlineSnapshot('"0"') + + await fireEvent.click(getByText('+1')) + expect(content()).toMatchInlineSnapshot('"1"') + + await fireEvent.click(getByText('+1000')) + expect(content()).toMatchInlineSnapshot('"1 тис."') + }) + + it('renders as a slot', async () => { + await fireEvent.click(getByText('Slots on')) + + refreshCounter() + + expect(content()).toMatchInlineSnapshot('"Count is: 0"') + + await fireEvent.click(getByText('+1')) + expect(content()).toMatchInlineSnapshot('"Count is: 1"') + + await fireEvent.click(getByText('+1000')) + expect(content()).toMatchInlineSnapshot('"Count is: 1K"') + + const slot = getByTestId('counter-slot') + const slotContent = withAbnormalSpacesReplaced(slot.textContent!) + expect(slotContent).toMatchInlineSnapshot('"1K"') + }) +}) diff --git a/test/components/FormattedNumberParts/counter.tsx b/test/components/FormattedNumberParts/counter.tsx new file mode 100644 index 0000000..f5f390a --- /dev/null +++ b/test/components/FormattedNumberParts/counter.tsx @@ -0,0 +1,43 @@ +import { defineComponent, ref } from 'vue' +import { + FormattedNumberParts, + type FormattedNumberPartsSlots, +} from '../../../dist/components' + +export const Counter = defineComponent(() => { + const count = ref(0) + + const incrementByOne = () => { + count.value++ + } + + const incrementByThousand = () => { + count.value += 1000 + } + + const reset = () => { + count.value = 0 + } + + return () => { + const slots: FormattedNumberPartsSlots = { + default: ({ parts }) => + parts.map((part) => + part.type === 'integer' ? {part.value} : part.value, + ), + } + + return ( +
+

+ + {slots} + +

+ + + +
+ ) + } +}) diff --git a/test/components/FormattedNumberParts/index.test.ts b/test/components/FormattedNumberParts/index.test.ts new file mode 100644 index 0000000..e334329 --- /dev/null +++ b/test/components/FormattedNumberParts/index.test.ts @@ -0,0 +1,58 @@ +import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest' +import { cleanup, fireEvent, render } from '@testing-library/vue' +import { + createVIntlPlugin, + withAbnormalSpacesReplaced, +} from '../../utils/index.ts' +import { Counter } from './counter.tsx' + +describe('FormattedNumberParts', () => { + afterAll(() => cleanup()) + + const vintl = createVIntlPlugin(['en-US', 'uk']) + const { plugin, controller, resetController } = vintl + + const { getByText, getByTestId } = render(Counter, { + global: { plugins: [plugin] }, + }) + + let counter: HTMLElement + const refreshCounter = () => (counter = getByTestId('counter')) + + const content = () => withAbnormalSpacesReplaced(counter.textContent!) + + beforeEach(async () => { + await fireEvent.click(getByText('Reset')) + refreshCounter() + }) + + afterEach(resetController) + + it('renders', async () => { + expect(content()).toBe('0') + + await fireEvent.click(getByText('+1')) + expect(content()).toBe('1') + + await fireEvent.click(getByText('+1000')) + expect(content()).toBe('1K') + + const integerParts = counter.querySelectorAll('b') + expect(integerParts).toHaveLength(1) + expect(integerParts[0].textContent).toBe('1') + }) + + it('changes locale', async () => { + await controller.changeLocale('uk') + + await fireEvent.click(getByText('+1')) + expect(content()).toBe('1') + + await fireEvent.click(getByText('+1000')) + expect(content()).toBe('1 тис.') + + const integerParts = counter.querySelectorAll('b') + expect(integerParts).toHaveLength(1) + expect(integerParts[0].textContent).toBe('1') + }) +}) diff --git a/test/components/FormattedPlural/index.test.ts b/test/components/FormattedPlural/index.test.ts new file mode 100644 index 0000000..8bab8f6 --- /dev/null +++ b/test/components/FormattedPlural/index.test.ts @@ -0,0 +1,97 @@ +import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest' +import { cleanup, fireEvent, render } from '@testing-library/vue' +import { createVIntlPlugin } from '../../utils/index.ts' +import { PluralDisplay } from './pluralDisplay.tsx' + +describe('FormattedPlural', () => { + afterAll(() => cleanup()) + + const vintl = createVIntlPlugin(['en-US', 'uk']) + const { plugin, controller, resetController } = vintl + + const { getByText, getByTestId } = render(PluralDisplay, { + global: { plugins: [plugin] }, + }) + + let display: HTMLElement + const refreshDisplay = () => (display = getByTestId('plural-display')) + + afterEach(resetController) + + beforeEach(async () => { + await fireEvent.click(getByText('Reset')) + refreshDisplay() + }) + + const content = () => display.textContent! + + it('renders', async () => { + // 0 => other + // 1 => one + // 2 => other + + expect(content()).toMatchInlineSnapshot('"other with value 0"') + + await fireEvent.click(getByText('+1')) + expect(content()).toMatchInlineSnapshot('"one with value 1"') + + await fireEvent.click(getByText('+1')) + expect(content()).toMatchInlineSnapshot('"other with value 2"') + }) + + it('respects type', async () => { + // 0 => other + // 1 => one + // 2 => two + // 3 => few + + await fireEvent.click(getByText('Switch to ordinal')) + expect(content()).toMatchInlineSnapshot('"other with value 0"') + + await fireEvent.click(getByText('+1')) + expect(content()).toMatchInlineSnapshot('"one with value 1"') + + await fireEvent.click(getByText('+1')) + expect(content()).toMatchInlineSnapshot('"two with value 2"') + + await fireEvent.click(getByText('+1')) + expect(content()).toMatchInlineSnapshot('"few with value 3"') + }) + + it('changes locale', async () => { + // 0 => many + // 1 => one + // 2 => few + + await controller.changeLocale('uk') + expect(content()).toMatchInlineSnapshot('"many with value 0"') + + await fireEvent.click(getByText('+1')) + expect(content()).toMatchInlineSnapshot('"one with value 1"') + + await fireEvent.click(getByText('+1')) + expect(content()).toMatchInlineSnapshot('"few with value 2"') + }) + + it('fallbacks correctly', async () => { + // 0 => many => other + // 1 => one => one + // 2 => few => other + + await controller.changeLocale('uk') + await fireEvent.click(getByText('Handle selection of slots')) + expect(content()).toMatchInlineSnapshot('"other with value 0"') + + await fireEvent.click(getByText('+1')) + expect(content()).toMatchInlineSnapshot('"one with value 1"') + + await fireEvent.click(getByText('+1')) + expect(content()).toMatchInlineSnapshot('"other with value 2"') + }) + + it('renders nothing if no slots handled', async () => { + // * => [nothing] + await fireEvent.click(getByText('Handle no slots')) + expect(content()).toMatchInlineSnapshot('""') + }) +}) diff --git a/test/components/FormattedPlural/pluralDisplay.tsx b/test/components/FormattedPlural/pluralDisplay.tsx new file mode 100644 index 0000000..0358b9d --- /dev/null +++ b/test/components/FormattedPlural/pluralDisplay.tsx @@ -0,0 +1,66 @@ +import { defineComponent, ref } from 'vue' +import { + FormattedPlural, + type FormattedPluralSlots, +} from '../../../dist/components' + +export const PluralDisplay = defineComponent(() => { + const count = ref(0) + const incrementByOne = () => (count.value += 1) + + const slotsHandling = ref<'full' | 'partial' | 'none'>('full') + const handleSelectionOfSlots = () => (slotsHandling.value = 'partial') + const handleNoSlots = () => (slotsHandling.value = 'none') + + const pluralType = ref('cardinal') + const switchToOrdinal = () => (pluralType.value = 'ordinal') + + const reset = () => { + count.value = 0 + slotsHandling.value = 'full' + pluralType.value = 'cardinal' + } + + return () => { + let slots: FormattedPluralSlots + + switch (slotsHandling.value) { + case 'full': + slots = { + zero: ({ value }) => `zero with value ${value}`, + one: ({ value }) => `one with value ${value}`, + two: ({ value }) => `two with value ${value}`, + few: ({ value }) => `few with value ${value}`, + many: ({ value }) => `many with value ${value}`, + other: ({ value }) => `other with value ${value}`, + } + break + case 'partial': + slots = { + one: ({ value }) => `one with value ${value}`, + other: ({ value }) => `other with value ${value}`, + } + break + case 'none': + slots = {} + break + } + + return ( + <> +

+ + {slots} + +

+ + + + + + + ) + } +}) diff --git a/test/components/FormattedRelativeTime/index.test.ts b/test/components/FormattedRelativeTime/index.test.ts new file mode 100644 index 0000000..0f432a7 --- /dev/null +++ b/test/components/FormattedRelativeTime/index.test.ts @@ -0,0 +1,67 @@ +import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest' +import { cleanup, fireEvent, render } from '@testing-library/vue' +import { + createVIntlPlugin, + withAbnormalSpacesReplaced, +} from '../../utils/index.ts' +import { RelativeTimeDisplay } from './relativeTimeDisplay.tsx' + +describe('FormattedRelativeTime', () => { + afterAll(() => cleanup()) + + const vintl = createVIntlPlugin(['en-US', 'uk']) + const { plugin, controller, resetController } = vintl + + const { getByText, getByTestId } = render(RelativeTimeDisplay, { + global: { plugins: [plugin] }, + }) + + let display: HTMLElement + const refreshDisplay = () => (display = getByTestId('time-display')) + + beforeEach(async () => { + await fireEvent.click(getByText('Reset')) + refreshDisplay() + }) + + afterEach(resetController) + + const content = () => withAbnormalSpacesReplaced(display.textContent!) + + it('renders', async () => { + expect(content()).toMatchInlineSnapshot('"in 0 seconds"') + + await fireEvent.click(getByText('+1')) + expect(content()).toMatchInlineSnapshot('"in 1 second"') + + await fireEvent.click(getByText('Use minutes')) + expect(content()).toMatchInlineSnapshot('"in 1 minute"') + }) + + it('changes locale', async () => { + await controller.changeLocale('uk') + expect(content()).toMatchInlineSnapshot('"через 0 секунд"') + + await fireEvent.click(getByText('+1')) + expect(content()).toMatchInlineSnapshot('"через 1 секунду"') + + await fireEvent.click(getByText('Use minutes')) + expect(content()).toMatchInlineSnapshot('"через 1 хвилину"') + }) + + it('renders as a slot', async () => { + await fireEvent.click(getByText('Slots on')) + refreshDisplay() + + expect(content()).toMatchInlineSnapshot('"Relative time is: in 0 seconds"') + + await fireEvent.click(getByText('+1')) + expect(content()).toMatchInlineSnapshot('"Relative time is: in 1 second"') + + await fireEvent.click(getByText('Use minutes')) + expect(content()).toMatchInlineSnapshot('"Relative time is: in 1 minute"') + + const slot = getByTestId('time-slot') + expect(slot.textContent!).toMatchInlineSnapshot('"in 1 minute"') + }) +}) diff --git a/test/components/FormattedRelativeTime/relativeTimeDisplay.tsx b/test/components/FormattedRelativeTime/relativeTimeDisplay.tsx new file mode 100644 index 0000000..8fb2203 --- /dev/null +++ b/test/components/FormattedRelativeTime/relativeTimeDisplay.tsx @@ -0,0 +1,55 @@ +import { defineComponent, ref } from 'vue' +import { + FormattedRelativeTime, + type FormattedRelativeTimeSlots, +} from '../../../dist/components' + +export const RelativeTimeDisplay = defineComponent(() => { + const amount = ref(0) + const incrementByOne = () => (amount.value += 1) + + const unit = ref('seconds') + const switchToMinutes = () => (unit.value = 'minutes') + + const useSlots = ref(false) + const enableSlots = () => (useSlots.value = true) + + const reset = () => { + amount.value = 0 + unit.value = 'seconds' + useSlots.value = false + } + + return () => { + let display: JSX.Element + + if (useSlots.value) { + display = ( + + { + { + default: ({ formattedValue }) => ( + <> + {'Relative time is: '} + {formattedValue} + + ), + } satisfies FormattedRelativeTimeSlots + } + + ) + } else { + display = + } + + return ( + <> +

{display}

+ + + + + + ) + } +}) diff --git a/test/utils/index.ts b/test/utils/index.ts new file mode 100644 index 0000000..c12699f --- /dev/null +++ b/test/utils/index.ts @@ -0,0 +1,33 @@ +import { type LocaleLoadEvent } from '../../dist/events' +import { createPlugin } from '../../dist/plugin' + +export function withAbnormalSpacesReplaced(value: string): string { + return value.replace(/[\u202F\u00A0]/g, ' ') +} + +export function createVIntlPlugin( + locales: string[], + loadLocale?: (e: LocaleLoadEvent) => void | Promise, +) { + const plugin = createPlugin({ + controllerOpts: { + locales: locales.map((tag) => ({ tag })), + listen: { localeload: loadLocale }, + }, + }) + + const controller = plugin.getOrCreateController() + + const initialState = { ...controller.$config } + + return { + plugin, + get controller() { + return controller + }, + async resetController() { + Object.assign(controller.$config, initialState) + await controller.waitUntilReady() + }, + } +} diff --git a/tsconfig.tests.json b/tsconfig.tests.json index 0b65670..dd53719 100644 --- a/tsconfig.tests.json +++ b/tsconfig.tests.json @@ -5,13 +5,24 @@ "module": "ESNext", + "emitDeclarationOnly": true, + "allowImportingTsExtensions": true, + "moduleResolution": "bundler", "esModuleInterop": true, "types": [], - "lib": ["ES2022", "DOM"] + "lib": ["ES2022", "DOM"], + + "jsx": "react-jsx", + "jsxImportSource": "vue" }, - "include": ["./vitest.config.ts", "./test/*.test.ts", "./tsconfig.tests.json"] + "include": [ + "./vitest.config.ts", + "./test/**/*.ts", + "./test/**/*.tsx", + "./tsconfig.tests.json" + ] } diff --git a/vitest.config.ts b/vitest.config.ts index 9e3222f..f5daaed 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,5 +1,6 @@ /// import { defineConfig } from 'vitest/config' +import tsconfig from './tsconfig.tests.json' export default defineConfig({ test: { @@ -8,4 +9,7 @@ export default defineConfig({ }, environment: 'happy-dom', }, + esbuild: { + tsconfigRaw: tsconfig as any, + }, }) From 3eaf0613f566421397f1a2bf612aeaf669502b1c Mon Sep 17 00:00:00 2001 From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com> Date: Mon, 10 Jul 2023 20:30:57 +0200 Subject: [PATCH 19/20] Allow to pass message payloads as is in createVIntlPlugin test util --- test/utils/index.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/test/utils/index.ts b/test/utils/index.ts index c12699f..6a9d3d8 100644 --- a/test/utils/index.ts +++ b/test/utils/index.ts @@ -7,12 +7,21 @@ export function withAbnormalSpacesReplaced(value: string): string { export function createVIntlPlugin( locales: string[], - loadLocale?: (e: LocaleLoadEvent) => void | Promise, + loadLocale?: + | ((e: LocaleLoadEvent) => void | Promise) + | Record | undefined>, ) { const plugin = createPlugin({ controllerOpts: { locales: locales.map((tag) => ({ tag })), - listen: { localeload: loadLocale }, + listen: { + localeload: + typeof loadLocale === 'function' || loadLocale == null + ? loadLocale + : (e) => { + e.addMessages(loadLocale[e.locale.tag] ?? {}) + }, + }, }, }) From b194662643b5cc4057d54a8a6ab5b42ec210aac8 Mon Sep 17 00:00:00 2001 From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com> Date: Fri, 22 Sep 2023 22:06:00 +0200 Subject: [PATCH 20/20] Add useMessage composable --- .changeset/tender-zoos-shop.md | 7 + src/runtime/index.ts | 1 + src/runtime/useMessages.ts | 161 ++++++++++++++++++ test/composables/useMessages/index.test.ts | 75 ++++++++ .../useMessages/messageDisplay.tsx | 92 ++++++++++ 5 files changed, 336 insertions(+) create mode 100644 .changeset/tender-zoos-shop.md create mode 100644 src/runtime/useMessages.ts create mode 100644 test/composables/useMessages/index.test.ts create mode 100644 test/composables/useMessages/messageDisplay.tsx diff --git a/.changeset/tender-zoos-shop.md b/.changeset/tender-zoos-shop.md new file mode 100644 index 0000000..9ddd35b --- /dev/null +++ b/.changeset/tender-zoos-shop.md @@ -0,0 +1,7 @@ +--- +'@vintl/vintl': minor +--- + +Add `useMessages` composable + +v5 introduces a new API that allows you to create messages more effectively. diff --git a/src/runtime/index.ts b/src/runtime/index.ts index 7273a40..242fd05 100644 --- a/src/runtime/index.ts +++ b/src/runtime/index.ts @@ -1 +1,2 @@ export { useVIntl } from './useVIntl.js' +export { useMessages, useMessage } from './useMessages.js' diff --git a/src/runtime/useMessages.ts b/src/runtime/useMessages.ts new file mode 100644 index 0000000..dd46ad9 --- /dev/null +++ b/src/runtime/useMessages.ts @@ -0,0 +1,161 @@ +import type { MessageDescriptor as MessageDescriptorBase } from '@formatjs/intl' +import type { FormatXMLElementFn, PrimitiveType } from 'intl-messageformat' +import { computed, isRef, reactive, type ComputedRef, type Ref } from 'vue' +import type { IntlController } from '../controller.ts' +import type { MessageValueType } from '../index.ts' +import { useVIntl } from './useVIntl.ts' + +type MaybeRef = T | Ref + +type PrimitiveValuesRecord = MaybeRef<{ + [key: string]: MaybeRef> +}> + +type ValuesRecord = MaybeRef<{ + [key: string]: MaybeRef< + PrimitiveType | RichTypes | FormatXMLElementFn + > +}> + +interface MessageDescriptor extends MessageDescriptorBase { + /** + * A record of the values for arguments used in the message. Can contain Vue + * references, which will be unwrapped, or be a reference itself. + */ + values?: ValuesRecord +} + +type MessageDescriptorOutput< + Descriptor extends MessageDescriptor, + RichTypes, +> = [Descriptor['values']] extends [undefined] + ? string + : Descriptor['values'] extends PrimitiveValuesRecord + ? string + : Array | RichTypes | string + +type MessageDescriptorsRecord = Record< + string, + MessageDescriptor +> + +type MessageDescriptorsRecordOutput< + Descriptor extends MessageDescriptorsRecord, + RichTypes, +> = { + [K in keyof Descriptor]: MessageDescriptorOutput +} + +function formatMessage< + Descriptor extends MessageDescriptor, + RichTypes, +>( + message: Descriptor, + vintl: IntlController, +): MessageDescriptorOutput { + const values = Object.create(null) + const rawInputs = message.values + + if (isRef(rawInputs)) { + Object.assign(values, rawInputs.value) + } else if (rawInputs != null) { + for (const k in rawInputs) { + const input = rawInputs[k] + values[k] = isRef(input) ? input.value : input + } + } + + return vintl.intl.formatMessage(message, values) +} + +/** + * Accepts a plain object of extended message descriptors, which may contain + * `values` alongside the message declaration itself. It then creates an object + * where each descriptor is mapped to a formatted. The object is reactive and + * message properties will be updating when the language, messages or values in + * the messages change. + * + * You can use `toRef` or `useMessages` to create read-only references for + * individual. + * + * @example + * const messages = useMessages({ + * farewell: { + * id: 'farewell', + * defaultMessage: 'Goodbye, {user}!', + * values: { + * user: computed(() => user.value.displayName), + * }, + * }, + * richText: { + * id: 'rich-text', + * defaultMessage: 'This text is red.', + * values: { + * red(children) { + * return h('span', { style: { color: 'red' } }, [children]) + * }, + * }, + * }, + * }) + * + * console.log(messages.farewell) // 'Goodbye, Andrea Rees!' + * + * @param messages A record of message descriptors. + * @returns A reactive map of messages. + */ +export function useMessages< + Descriptor extends MessageDescriptorsRecord, + RichTypes = MessageValueType, +>(messages: Descriptor) { + const vintl = useVIntl() + + type PreOutput = { + [K in keyof MessageDescriptorsRecordOutput< + Descriptor, + RichTypes + >]: ComputedRef[K]> + } + + const target: PreOutput = Object.create(null) + + for (const key of Object.keys(messages) as (keyof Descriptor)[]) { + const message = messages[key] + + type Message = Descriptor[typeof key] + + target[key] = computed(() => + formatMessage(message, vintl), + ) + } + + return reactive(target) +} + +/** + * Accepts an extended message descriptor, which may contain `values` alongside + * the message declaration itself. It then returns a read-only reference that + * gets updated when the language, messages or the values for the message + * change. + * + * @example + * const helloMessage = useMessage({ + * id: 'hello', + * defaultMessage: 'Hello, {user}!', + * values: { + * user: computed(() => user.value.displayName), + * }, + * }) + * + * console.log(helloMessage.value) // 'Hello, Andrea Rees!' + * + * @param message A message descriptor. + * @returns A read-only reference to the actual formatted message. + */ +export function useMessage< + Descriptor extends MessageDescriptor, + RichTypes = MessageValueType, +>(message: Descriptor) { + const vintl = useVIntl() + + return computed(() => formatMessage(message, vintl)) +} diff --git a/test/composables/useMessages/index.test.ts b/test/composables/useMessages/index.test.ts new file mode 100644 index 0000000..3ab910d --- /dev/null +++ b/test/composables/useMessages/index.test.ts @@ -0,0 +1,75 @@ +import { cleanup, fireEvent, render } from '@testing-library/vue' +import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest' +import { createVIntlPlugin } from '../../utils/index.ts' +import { messagesPayload, MessageDisplay } from './messageDisplay.tsx' + +describe('useMessages', () => { + afterAll(() => cleanup()) + + const vintl = createVIntlPlugin(['en-US', 'uk'], messagesPayload) + + const { plugin, controller, resetController } = vintl + + afterEach(resetController) + + const { getByText, getByTestId } = render(MessageDisplay, { + global: { plugins: [plugin] }, + }) + + // const display = getByTestId('message-display') + const messageContainer = getByTestId('message-container') + const warningContainer = getByTestId('warning-container') + + beforeEach(() => fireEvent.click(getByText('Reset'))) + + const messageHTML = () => messageContainer.innerHTML + const warningHTML = () => warningContainer.innerHTML + + it('renders', async () => { + expect(messageHTML()).toMatchInlineSnapshot( + '"Hello, Andrei!You don\'t have unread messages."', + ) + expect(warningHTML()).toMatchInlineSnapshot( + '"Warning! This is a warning."', + ) + + await fireEvent.click(getByText('Add unread')) + expect(messageHTML()).toMatchInlineSnapshot( + '"Hello, Andrei!You have 1 unread message."', + ) + + await fireEvent.click(getByText('Set intent to goodbye')) + expect(messageHTML()).toMatchInlineSnapshot( + '"Goodbye, Andrei!You have 1 unread message."', + ) + }) + + it('changes locale', async () => { + await controller.changeLocale('uk') + expect(messageHTML()).toMatchInlineSnapshot( + '"Привіт, Andrei!У вас немає непрочитаних повідомлень."', + ) + expect(warningHTML()).toMatchInlineSnapshot( + '"Попередження! Це попередження."', + ) + + // 1 = one + await fireEvent.click(getByText('Add unread')) + expect(messageHTML()).toMatchInlineSnapshot( + '"Привіт, Andrei!У вас є 1 непрочитане повідомлення."', + ) + + // 2 = few + await fireEvent.click(getByText('Add unread')) + expect(messageHTML()).toMatchInlineSnapshot( + '"Привіт, Andrei!У вас є 2 непрочитаних повідомлення."', + ) + + // safe to assume it will continue to handle number updates + + await fireEvent.click(getByText('Set intent to goodbye')) + expect(messageHTML()).toMatchInlineSnapshot( + '"До побачення, Andrei!У вас є 2 непрочитаних повідомлення."', + ) + }) +}) diff --git a/test/composables/useMessages/messageDisplay.tsx b/test/composables/useMessages/messageDisplay.tsx new file mode 100644 index 0000000..73d10d6 --- /dev/null +++ b/test/composables/useMessages/messageDisplay.tsx @@ -0,0 +1,92 @@ +import { computed, defineComponent, ref } from 'vue' +import { useMessage, useMessages } from '../../../dist/index' + +export const messagesPayload: Record> = { + 'en-US': { + greeting: 'Hello, {name}!', + farewell: 'Goodbye, {name}!', + inboxMessages: + "You {count, plural, =0 {don't have unread messages} one {have # unread message} other {have # unread messages}}.", + warnText: 'Warning! This is a warning.', + }, + uk: { + greeting: 'Привіт, {name}!', + farewell: 'До побачення, {name}!', + inboxMessages: + 'У вас {count, plural, =0 {немає непрочитаних повідомлень} one {є # непрочитане повідомлення} few {є # непрочитаних повідомлення} many {є # непрочитаних повідомлень} other {є непрочитаних повідомлень}}.', + warnText: 'Попередження! Це попередження.', + }, +} + +export const MessageDisplay = defineComponent(() => { + const incrementByOne = () => (unreadMessages.value += 1) + + const intent = ref<'hello' | 'goodbye'>('hello') + const setIntentToHello = () => (intent.value = 'hello') + const setIntentToGoodbye = () => (intent.value = 'goodbye') + + const reset = () => { + name.value = 'Andrei' + unreadMessages.value = 0 + intent.value = 'hello' + } + + const name = ref('Andrei') + + const unreadMessages = ref(0) + + const messages = useMessages({ + inboxMessages: { + id: 'inboxMessages', + defaultMessage: messagesPayload['en-US'].inboxMessages, + values: { count: unreadMessages }, + }, + warnText: { + id: 'warnText', + defaultMessage: messagesPayload['en-US'].warnText, + values: { + b(chunks) { + return {chunks} + }, + }, + }, + }) + + const helloMessage = useMessage({ + id: 'greeting', + defaultMessage: messagesPayload['en-US'].greeting, + values: { name }, + }) + + const goodbyeMessage = useMessage({ + id: 'farewell', + defaultMessage: messagesPayload['en-US'].farewell, + values: { name }, + }) + + const intentMessage = computed(() => + intent.value === 'hello' ? helloMessage.value : goodbyeMessage.value, + ) + + const Warning = defineComponent(() => () => messages.warnText) + + return () => { + return ( + <> +

+

+ {intentMessage.value} + {messages.inboxMessages} +
+
+ +
+

+ + + + + + ) + } +})