Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,25 @@
"sourceType": "module"
},
"parser": "@babel/eslint-parser",
"settings": { "react": { "version": "detect" } }
"settings": {
"react": { "version": "detect" },
"import/extensions": [".js", ".jsx", ".ts", ".tsx"],
"import/parsers": {
"@babel/eslint-parser": [".js", ".jsx", ".ts", ".tsx"]
},
"import/resolver": {
"node": {
"extensions": [".js", ".jsx", ".ts", ".tsx"]
}
}
},
"overrides": [
{
"files": ["**/*.ts", "**/*.tsx"],
"rules": {
"no-undef": "off",
"no-unused-vars": "off"
}
}
]
}
72 changes: 34 additions & 38 deletions __test__/Interpolate.test.js → __test__/Interpolate.test.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,46 @@
/* eslint-disable react/display-name */
import React from 'react'
import { render } from '@testing-library/react'
import Interpolate, { SYNTAX_I18NEXT } from '../src'
import React from 'react'
import Interpolate, { SYNTAX_I18NEXT, type InterpolateProps } from '../src'
Comment thread
gnapse marked this conversation as resolved.

Interpolate.defaultProps = {
graceful: false,
afterEach(() => {
jest.restoreAllMocks()
})

const suppressConsole = () => {
jest.spyOn(console, 'warn').mockImplementation(() => undefined)
jest.spyOn(console, 'error').mockImplementation(() => undefined)
}

const surpressConsole = () => {
const w = jest.spyOn(console, 'warn').mockImplementation()
const e = jest.spyOn(console, 'error').mockImplementation()
type RenderSuccessProps = InterpolateProps & {
expected: string
}

return () => {
w.mockRestore()
e.mockRestore()
}
type RenderErrorProps = InterpolateProps & {
expectedError: string
}

describe('Interpolate', () => {
function renderTest({ expected, ...props }) {
const { container } = render(<Interpolate {...props} />)
function renderTest({ expected, graceful = false, ...props }: RenderSuccessProps) {
const { container } = render(<Interpolate {...props} graceful={graceful} />)
expect(container.innerHTML).toEqual(expected)
}

test('when no mapping is provide', () => {
const restore = surpressConsole() // Interpolate will output warning when no mapping is provided
suppressConsole()

renderTest({
string: '<h1>hello <b>{name}</b></h1><br/>. welcome to todoist',
expected: '<h1>hello <b>{name}</b></h1><br>. welcome to todoist',
})

restore()
})

test('tag mapping', () =>
renderTest({
string: '<h1>hello <b>steven</b></h1>. welcome to todoist',
mapping: {
b: (child) => <i>{child}</i>,
h1: (child) => <h2>{child}</h2>,
b: (children) => <i>{children}</i>,
h1: (children) => <h2>{children}</h2>,
},
expected: '<h2>hello <i>steven</i></h2>. welcome to todoist',
}))
Expand Down Expand Up @@ -77,25 +78,24 @@ describe('Interpolate', () => {
renderTest({
string: '<h1>hello <b>{name}</b></h1>.<br/> welcome to todoist',
mapping: {
h1: (child) => <h2>{child}</h2>,
b: (child) => <i>{child}</i>,
h1: (children) => <h2>{children}</h2>,
b: (children) => <i>{children}</i>,
name: 'steven',
br: <hr />,
},
expected: '<h2>hello <i>steven</i></h2>.<hr> welcome to todoist',
}))

test('combination of mapping with function component', () => {
// eslint-disable-next-line
const Subheader = ({ children }) => {
const Subheader = ({ children }: React.PropsWithChildren) => {
return <h2 className="subheader">{children}</h2>
}

renderTest({
string: '<h1>hello <b>{name}</b></h1>.<br/> welcome to todoist',
mapping: {
h1: (child) => <Subheader>{child}</Subheader>,
b: (child) => <i>{child}</i>,
h1: (children) => <Subheader>{children}</Subheader>,
b: (children) => <i>{children}</i>,
name: 'steven',
br: <hr />,
},
Expand Down Expand Up @@ -124,28 +124,26 @@ describe('Interpolate', () => {
'hello &lt;script&gt;window.xss = 1&lt;/script&gt;&lt;script&gt;window.xss = 1&lt;/script&gt; welcome to todoist',
})

expect(window.css).toBeUndefined()
expect((window as typeof window & { xss?: number }).xss).toBeUndefined()
})

test('when graceful flag is on and string contains syntax error, interpolate should return the original string and should not throw error', () => {
const restore = surpressConsole()
suppressConsole()

renderTest({
string: '</h1>',
expected: '&lt;/h1&gt;',
graceful: true,
})

restore()
})

test('using SYNTAX_I18NEXT', () => {
renderTest({
syntax: SYNTAX_I18NEXT,
string: '<0>hello <b>{{name}}</b></0>.<br/> welcome to todoist',
mapping: {
0: (child) => <h2 className="subheader">{child}</h2>,
b: (child) => <i>{child}</i>,
0: (children) => <h2 className="subheader">{children}</h2>,
b: (children) => <i>{children}</i>,
name: 'steven',
br: <hr />,
},
Expand All @@ -155,17 +153,15 @@ describe('Interpolate', () => {
})

describe('Interpolate: error cases', () => {
let restore
beforeAll(() => {
restore = surpressConsole()
})
afterAll(() => {
restore()
beforeEach(() => {
suppressConsole()
})

function renderTest({ expectedError, ...props }) {
function renderTest({ expectedError, graceful = false, ...props }: RenderErrorProps) {
return () => {
expect(() => render(<Interpolate {...props} />)).toThrow(expectedError)
expect(() => render(<Interpolate {...props} graceful={graceful} />)).toThrow(
expectedError,
)
}
}

Expand Down
34 changes: 34 additions & 0 deletions __test__/package-smoke.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const React = require('react')
const { renderToStaticMarkup } = require('react-dom/server')

function assert(condition, message) {
if (!condition) {
throw new Error(message)
}
}

async function main() {
const cjsPackage = require('..')
const esmPackage = await import('../dist/react-interpolate.mjs')

assert(typeof cjsPackage.default === 'function', 'CJS default export should be a function')
assert(typeof esmPackage.default === 'function', 'ESM default export should be a function')
assert(cjsPackage.TOKEN_PLACEHOLDER === esmPackage.TOKEN_PLACEHOLDER, 'Named exports should match')

const html = renderToStaticMarkup(
React.createElement(cjsPackage.default, {
string: '<b>{name}</b>',
mapping: {
b: React.createElement('strong'),
name: 'William',
},
}),
)

assert(html === '<strong>William</strong>', 'Built package should render expected HTML')
}

main().catch((error) => {
console.error(error)
process.exit(1)
})
18 changes: 18 additions & 0 deletions __test__/package-types.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import pkg = require('@doist/react-interpolate')

import type { ReactNode } from 'react'

const mapping: pkg.Mapping = {
b: (children?: ReactNode) => children ?? null,
name: 'William',
}

const props: pkg.InterpolateProps = {
string: '<b>{{name}}</b>',
syntax: pkg.SYNTAX_I18NEXT,
mapping,
graceful: true,
}

pkg.default(props)
pkg.TOKEN_PLACEHOLDER
23 changes: 23 additions & 0 deletions __test__/package-types.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Interpolate, {
SYNTAX_I18NEXT,
TOKEN_PLACEHOLDER,
type InterpolateProps,
type Mapping,
} from '@doist/react-interpolate'
import type { ReactNode } from 'react'

const mapping: Mapping = {
b: (children?: ReactNode) => children ?? null,
name: 'William',
}

const props: InterpolateProps = {
string: '<b>{{name}}</b>',
syntax: SYNTAX_I18NEXT,
mapping,
graceful: true,
}

void Interpolate
void TOKEN_PLACEHOLDER
void props
13 changes: 13 additions & 0 deletions __test__/parser.test.js → __test__/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,16 @@ describe('parser: error handling', () => {
})
})
})

describe('parser: custom syntax validation', () => {
test('syntax rules must use global regexes', () => {
expect(() => {
parser('hello {name}', [
{
type: 'TOKEN_PLACEHOLDER',
regex: /{\s*(\w+)\s*}/,
},
])
}).toThrow('Syntax rule regex must use the global flag')
})
})
16 changes: 16 additions & 0 deletions __test__/tsconfig.package-cjs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"skipLibCheck": true,
"baseUrl": "..",
"noEmit": true,
"paths": {
"@doist/react-interpolate": ["./dist/index.d.ts"]
},
"types": ["node", "react"]
},
"files": ["package-types.cts"]
}
16 changes: 16 additions & 0 deletions __test__/tsconfig.package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"skipLibCheck": true,
"baseUrl": "..",
"noEmit": true,
"paths": {
"@doist/react-interpolate": ["./dist/index.d.ts"]
},
"types": ["node", "react"]
},
"files": ["package-types.mts"]
}
1 change: 1 addition & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module.exports = function (api) {
},
],
'@babel/react',
'@babel/preset-typescript',
],
plugins: ['@babel/plugin-transform-runtime'],
}
Expand Down
Loading
Loading