From 9fa4da499774a960fb99343f812a6411a633b122 Mon Sep 17 00:00:00 2001 From: 49659410+tx0c <> Date: Mon, 26 Jun 2023 23:53:42 +0000 Subject: [PATCH 01/62] feat(search-version): add new search version for web-next testing --- src/components/Search/SearchQuickResult/gql.ts | 12 ++++++++++-- .../Search/SearchQuickResult/index.tsx | 18 ++++++++++++++++-- src/views/Search/AggregateResults/Articles.tsx | 3 ++- src/views/Search/AggregateResults/Tags.tsx | 3 ++- src/views/Search/AggregateResults/Users.tsx | 3 ++- src/views/Search/AggregateResults/gql.ts | 6 ++++++ src/views/Search/AggregateResults/index.tsx | 7 ++++--- 7 files changed, 42 insertions(+), 10 deletions(-) diff --git a/src/components/Search/SearchQuickResult/gql.ts b/src/components/Search/SearchQuickResult/gql.ts index 6293954c27..ef12409e59 100644 --- a/src/components/Search/SearchQuickResult/gql.ts +++ b/src/components/Search/SearchQuickResult/gql.ts @@ -4,7 +4,7 @@ import { TagDigest } from '~/components/TagDigest' import { UserDigest } from '~/components/UserDigest' export const QUICK_RESULT = gql` - query QuickResult($key: String!) { + query QuickResult($key: String!, $version: SearchAPIVersion = v20230601) { user: search( input: { type: User @@ -12,6 +12,7 @@ export const QUICK_RESULT = gql` record: true first: 5 key: $key + version: $version } ) { edges { @@ -24,7 +25,14 @@ export const QUICK_RESULT = gql` } } tag: search( - input: { type: Tag, quicksearch: true, record: true, first: 5, key: $key } + input: { + type: Tag + quicksearch: true + record: true + first: 5 + key: $key + version: $version + } ) { edges { cursor diff --git a/src/components/Search/SearchQuickResult/index.tsx b/src/components/Search/SearchQuickResult/index.tsx index b600377232..48c04f2942 100644 --- a/src/components/Search/SearchQuickResult/index.tsx +++ b/src/components/Search/SearchQuickResult/index.tsx @@ -7,7 +7,15 @@ import { SEARCH_START_FLAG, } from '~/common/enums' import { analytics, toPath } from '~/common/utils' -import { Media, Menu, Spinner, TagDigest, UserDigest } from '~/components' +import { + Media, + Menu, + // Spacer, + Spinner, + TagDigest, + UserDigest, + useRoute, +} from '~/components' import { QuickResultQuery } from '~/gql/graphql' import { QUICK_RESULT } from './gql' @@ -23,6 +31,9 @@ interface QuickSearchProps { } export const SearchQuickResult = (props: QuickSearchProps) => { + const { getQuery } = useRoute() + const version = getQuery('version') + const { searchKey, inPage, activeItem, onUpdateData, closeDropdown } = props const client = useApolloClient() const [data, setData] = useState() @@ -63,7 +74,10 @@ export const SearchQuickResult = (props: QuickSearchProps) => { // https://github.com/apollographql/apollo-client/issues/5912 const response = await client.query({ query: QUICK_RESULT, - variables: { key: searchKey }, + variables: { + key: searchKey, + version: version === '' ? undefined : version, + }, fetchPolicy: 'no-cache', }) analytics.trackEvent('load_more', { diff --git a/src/views/Search/AggregateResults/Articles.tsx b/src/views/Search/AggregateResults/Articles.tsx index 515dcc2608..ef2beb8136 100644 --- a/src/views/Search/AggregateResults/Articles.tsx +++ b/src/views/Search/AggregateResults/Articles.tsx @@ -28,6 +28,7 @@ import { SEARCH_AGGREGATE_ARTICLES_PUBLIC } from './gql' const AggregateArticleResults = () => { const { getQuery } = useRoute() const q = getQuery('q') + const version = getQuery('version') /** * Data Fetching @@ -37,7 +38,7 @@ const AggregateArticleResults = () => { usePublicQuery( SEARCH_AGGREGATE_ARTICLES_PUBLIC, { - variables: { key: q }, + variables: { key: q, version: version === '' ? undefined : version }, } ) diff --git a/src/views/Search/AggregateResults/Tags.tsx b/src/views/Search/AggregateResults/Tags.tsx index 6dba9be476..88142f0f8e 100644 --- a/src/views/Search/AggregateResults/Tags.tsx +++ b/src/views/Search/AggregateResults/Tags.tsx @@ -24,6 +24,7 @@ import styles from './styles.module.css' const AggregateTagResults = () => { const { getQuery } = useRoute() const q = getQuery('q') + const version = getQuery('version') /** * Data Fetching @@ -32,7 +33,7 @@ const AggregateTagResults = () => { const { data, loading, fetchMore } = usePublicQuery( SEARCH_AGGREGATE_TAGS_PUBLIC, - { variables: { key: q } } + { variables: { key: q, version: version === '' ? undefined : version } } ) useEffect(() => { diff --git a/src/views/Search/AggregateResults/Users.tsx b/src/views/Search/AggregateResults/Users.tsx index cdbe452da3..edd494bdfd 100644 --- a/src/views/Search/AggregateResults/Users.tsx +++ b/src/views/Search/AggregateResults/Users.tsx @@ -24,6 +24,7 @@ import styles from './styles.module.css' const AggregateUserResults = () => { const { getQuery } = useRoute() const q = getQuery('q') + const version = getQuery('version') /** * Data Fetching @@ -32,7 +33,7 @@ const AggregateUserResults = () => { const { data, loading, fetchMore } = usePublicQuery( SEARCH_AGGREGATE_USERS_PUBLIC, - { variables: { key: q } } + { variables: { key: q, version: version === '' ? undefined : version } } ) useEffect(() => { diff --git a/src/views/Search/AggregateResults/gql.ts b/src/views/Search/AggregateResults/gql.ts index b2fdb87d58..36763b009f 100644 --- a/src/views/Search/AggregateResults/gql.ts +++ b/src/views/Search/AggregateResults/gql.ts @@ -7,6 +7,7 @@ export const SEARCH_AGGREGATE_ARTICLES_PUBLIC = gql` $key: String! $first: first_Int_min_0 = 30 $after: String + $version: SearchAPIVersion = v20230601 ) { search( input: { @@ -15,6 +16,7 @@ export const SEARCH_AGGREGATE_ARTICLES_PUBLIC = gql` first: $first key: $key after: $after + version: $version } ) { totalCount @@ -41,6 +43,7 @@ export const SEARCH_AGGREGATE_TAGS_PUBLIC = gql` $key: String! $first: first_Int_min_0 = 30 $after: String + $version: SearchAPIVersion = v20230601 ) { search( input: { @@ -49,6 +52,7 @@ export const SEARCH_AGGREGATE_TAGS_PUBLIC = gql` first: $first key: $key after: $after + version: $version } ) { pageInfo { @@ -74,6 +78,7 @@ export const SEARCH_AGGREGATE_USERS_PUBLIC = gql` $key: String! $first: first_Int_min_0 = 30 $after: String + $version: SearchAPIVersion = v20230601 ) { search( input: { @@ -82,6 +87,7 @@ export const SEARCH_AGGREGATE_USERS_PUBLIC = gql` first: $first key: $key after: $after + version: $version } ) { pageInfo { diff --git a/src/views/Search/AggregateResults/index.tsx b/src/views/Search/AggregateResults/index.tsx index 883f231b30..0afb2629a4 100644 --- a/src/views/Search/AggregateResults/index.tsx +++ b/src/views/Search/AggregateResults/index.tsx @@ -27,6 +27,7 @@ const AggregateResults = () => { getSearchType(getQuery('type')) || Type.ARTICLE ) const q = getQuery('q') + const version = getQuery('version') const isArticle = type === Type.ARTICLE const isUser = type === Type.USER @@ -41,17 +42,17 @@ const AggregateResults = () => { useEffect(() => { client.query({ query: SEARCH_AGGREGATE_ARTICLES_PUBLIC, - variables: { key: q }, + variables: { key: q, version: version === '' ? undefined : version }, fetchPolicy: 'network-only', }) client.query({ query: SEARCH_AGGREGATE_USERS_PUBLIC, - variables: { key: q }, + variables: { key: q, version: version === '' ? undefined : version }, fetchPolicy: 'network-only', }) client.query({ query: SEARCH_AGGREGATE_TAGS_PUBLIC, - variables: { key: q }, + variables: { key: q, version: version === '' ? undefined : version }, fetchPolicy: 'network-only', }) }, [q]) From a8cef313799899bd944ef8244a36186c4042fc16 Mon Sep 17 00:00:00 2001 From: robertu <4065233+robertu7@users.noreply.github.com> Date: Thu, 2 Nov 2023 20:58:54 +0800 Subject: [PATCH 02/62] feat(test): add tests for utils/datetime --- src/common/utils/datetime/absolute.test.ts | 71 ++++++++++++++++++++++ src/common/utils/datetime/relative.test.ts | 61 +++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 src/common/utils/datetime/absolute.test.ts create mode 100644 src/common/utils/datetime/relative.test.ts diff --git a/src/common/utils/datetime/absolute.test.ts b/src/common/utils/datetime/absolute.test.ts new file mode 100644 index 0000000000..32ae623af3 --- /dev/null +++ b/src/common/utils/datetime/absolute.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, it } from 'vitest' + +import toAbsoluteDate from './absolute' + +describe('datetime/absolute', () => { + it('should parse a string date', () => { + const date = '2022-01-01' + const result = toAbsoluteDate(date, 'en') + expect(typeof result).toBe('string') + }) + + it("should format today's date correctly", () => { + const date = new Date() + const result = toAbsoluteDate(date, 'en') + expect(result).toContain('Today') + }) + + it("should format yesterday's date correctly", () => { + const date = new Date() + date.setDate(date.getDate() - 1) + const result = toAbsoluteDate(date, 'en') + expect(result).toContain('Yesterday') + }) + + it("should format this year's date correctly", () => { + // two days ago + const date = new Date() + date.setDate(date.getDate() - 2) + + const thisYear = new Date().getFullYear() + const thisYearDate = new Date(`${thisYear}-01-01`) + + // skip test + if (thisYearDate > date) { + return + } + + const result = toAbsoluteDate(thisYearDate, 'en') + expect(result).toBe('Jan 1') + }) + + it('should format a date in the past correctly', () => { + const date = new Date('2021-01-01') + const result = toAbsoluteDate(date, 'en') + expect(result).toBe('Jan 1, 2021') + }) + + it('should format a date in the future correctly', () => { + const now = new Date() + const date = new Date(now.getTime() + 1000 * 60 * 60 * 24 * 365) + const result = toAbsoluteDate(date, 'en') + expect(result).toContain(date.getFullYear()) + }) + + it('should format different types of dates correctly', () => { + // string + const stringDate = '2021-01-01' + const stringResult = toAbsoluteDate(stringDate, 'en') + expect(stringResult).toBe('Jan 1, 2021') + + // number + const numberDate = 1609459200000 + const numberResult = toAbsoluteDate(numberDate, 'en') + expect(numberResult).toBe('Jan 1, 2021') + + // Date + const date = new Date('2021-01-01') + const dateResult = toAbsoluteDate(date, 'en') + expect(dateResult).toBe('Jan 1, 2021') + }) +}) diff --git a/src/common/utils/datetime/relative.test.ts b/src/common/utils/datetime/relative.test.ts new file mode 100644 index 0000000000..956bdb5092 --- /dev/null +++ b/src/common/utils/datetime/relative.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from 'vitest' + +import toRelativeDate from './relative' + +describe('datetime/relative', () => { + it('should parse a string date', () => { + const date = '2022-01-01' + const result = toRelativeDate(date, 'en') + expect(typeof result).toBe('string') + }) + + it('should format a date within this minute correctly', () => { + const date = new Date() + const result = toRelativeDate(date, 'en') + expect(result).toBe('just now') + }) + + it('should format a date within this hour but not this minute correctly', () => { + const now = new Date() + const date = new Date(now.getTime() - 1000 * 60) + const result = toRelativeDate(date, 'en') + expect(result).toBe('1 minute ago') + }) + + it('should format a date within today but not this hour correctly', () => { + const now = new Date() + const date = new Date(now.getTime() - 1000 * 60 * 60) + const result = toRelativeDate(date, 'en') + expect(result).toBe('1 hour ago') + }) + + it('should format a date within this week but not today correctly', () => { + const now = new Date() + const date = new Date(now.getTime() - 1000 * 60 * 60 * 24) + const result = toRelativeDate(date, 'en') + expect(result).toBe('1 day ago') + }) + + it('should format a date within this year but not this week correctly', () => { + const date = new Date('2021-01-01') + const result = toRelativeDate(date, 'en') + expect(result).toBe('Jan 1, 2021') + }) + + it('should format different types of dates correctly', () => { + // string + const stringDate = '2021-01-01' + const stringResult = toRelativeDate(stringDate, 'en') + expect(stringResult).toBe('Jan 1, 2021') + + // number + const numberDate = 1609459200000 + const numberResult = toRelativeDate(numberDate, 'en') + expect(numberResult).toBe('Jan 1, 2021') + + // Date + const date = new Date('2021-01-01') + const dateResult = toRelativeDate(date, 'en') + expect(dateResult).toBe('Jan 1, 2021') + }) +}) From 602cc7150e22203414281cab658cabd8679440c3 Mon Sep 17 00:00:00 2001 From: robertu <4065233+robertu7@users.noreply.github.com> Date: Thu, 2 Nov 2023 21:17:28 +0800 Subject: [PATCH 03/62] feat(test): add tests for utils/number --- src/common/utils/datetime/absolute.test.ts | 2 +- src/common/utils/datetime/relative.test.ts | 2 +- src/common/utils/number/abbr.test.ts | 64 ++++++++++++++++++++++ src/common/utils/number/index.test.ts | 38 +++++++++++++ src/common/utils/number/index.ts | 2 +- 5 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 src/common/utils/number/abbr.test.ts create mode 100644 src/common/utils/number/index.test.ts diff --git a/src/common/utils/datetime/absolute.test.ts b/src/common/utils/datetime/absolute.test.ts index 32ae623af3..0284ac28e4 100644 --- a/src/common/utils/datetime/absolute.test.ts +++ b/src/common/utils/datetime/absolute.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' import toAbsoluteDate from './absolute' -describe('datetime/absolute', () => { +describe('utils/datetime/absolute', () => { it('should parse a string date', () => { const date = '2022-01-01' const result = toAbsoluteDate(date, 'en') diff --git a/src/common/utils/datetime/relative.test.ts b/src/common/utils/datetime/relative.test.ts index 956bdb5092..9892ec03bc 100644 --- a/src/common/utils/datetime/relative.test.ts +++ b/src/common/utils/datetime/relative.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' import toRelativeDate from './relative' -describe('datetime/relative', () => { +describe('utils/datetime/relative', () => { it('should parse a string date', () => { const date = '2022-01-01' const result = toRelativeDate(date, 'en') diff --git a/src/common/utils/number/abbr.test.ts b/src/common/utils/number/abbr.test.ts new file mode 100644 index 0000000000..ea4c0412aa --- /dev/null +++ b/src/common/utils/number/abbr.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from 'vitest' + +import { abbr } from './abbr' + +describe('utils/number/abbr', () => { + it('should abbreviate thousands correctly', () => { + expect(abbr(1500, 2)).toBe('1.5k') + expect(abbr(1550, 2)).toBe('1.55k') + expect(abbr(1000, 2)).toBe('1k') + expect(abbr(1001, 2)).toBe('1k') + expect(abbr(1001.5, 2)).toBe('1k') + }) + + it('should abbreviate millions correctly', () => { + expect(abbr(1500000, 2)).toBe('1.5m') + expect(abbr(1554000, 2)).toBe('1.55m') + expect(abbr(1555000, 2)).toBe('1.56m') + expect(abbr(1000000, 2)).toBe('1m') + expect(abbr(1001400, 2)).toBe('1m') + expect(abbr(1001321.5, 2)).toBe('1m') + }) + + it('should abbreviate billions correctly', () => { + expect(abbr(1500000000, 2)).toBe('1.5b') + expect(abbr(1550000000, 2)).toBe('1.55b') + expect(abbr(1000000000, 2)).toBe('1b') + expect(abbr(1001000000, 2)).toBe('1b') + expect(abbr(1001000000.5, 2)).toBe('1b') + }) + + it('should abbreviate trillions correctly', () => { + expect(abbr(1500000000000, 2)).toBe('1.5t') + expect(abbr(1550000000000, 2)).toBe('1.55t') + expect(abbr(1000000000000, 2)).toBe('1t') + expect(abbr(1001000000000, 2)).toBe('1t') + expect(abbr(1001000000000.5, 2)).toBe('1t') + }) + + it('should abbreviate negative numbers correctly', () => { + expect(abbr(-1500, 2)).toBe('-1.5k') + expect(abbr(-1550, 2)).toBe('-1.55k') + expect(abbr(-1000, 2)).toBe('-1k') + expect(abbr(-1001, 2)).toBe('-1k') + expect(abbr(-1001.5, 2)).toBe('-1k') + }) + + it('should abbreviate numbers with no decimal places correctly', () => { + expect(abbr(1500, 0)).toBe('2k') + expect(abbr(1540, 0)).toBe('2k') + expect(abbr(1550, 0)).toBe('2k') + expect(abbr(1000, 0)).toBe('1k') + expect(abbr(1001, 0)).toBe('1k') + expect(abbr(1001.5, 0)).toBe('1k') + }) + + it('should abbreviate numbers with 3 decimal place correctly', () => { + expect(abbr(1500, 3)).toBe('1.5k') + expect(abbr(1540, 3)).toBe('1.54k') + expect(abbr(1550, 3)).toBe('1.55k') + expect(abbr(1000, 3)).toBe('1k') + expect(abbr(1001, 3)).toBe('1.001k') + expect(abbr(1001.5, 3)).toBe('1.002k') + }) +}) diff --git a/src/common/utils/number/index.test.ts b/src/common/utils/number/index.test.ts new file mode 100644 index 0000000000..504fc6990f --- /dev/null +++ b/src/common/utils/number/index.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from 'vitest' + +import { numPrefix, numRound } from './' + +describe('utils/number/numPrefix', () => { + it('should add a plus sign to positive numbers', () => { + const result = numPrefix(1500) + expect(result).toBe('+1500') + }) + + it('should not add a plus sign to zero', () => { + const result = numPrefix(0) + expect(result).toBe('0') + }) + + it('should not add a plus sign to negative numbers', () => { + const result = numPrefix(-2000) + expect(result).toBe('-2000') + }) +}) + +describe('utils/number/numRound', () => { + it('should round to the correct decimal places', () => { + expect(numRound(123.4563, 2)).toBe(123.46) + expect(numRound(123.4563, 3)).toBe(123.456) + expect(numRound(123.4333, 1)).toBe(123.4) + }) + + it('should round to the correct decimal places when the number is negative', () => { + const result = numRound(-123.456, 2) + expect(result).toBe(-123.46) + }) + + it('should round to the correct decimal places when the number is zero', () => { + const result = numRound(0, 2) + expect(result).toBe(0) + }) +}) diff --git a/src/common/utils/number/index.ts b/src/common/utils/number/index.ts index 802ab682d9..f86dbd84be 100644 --- a/src/common/utils/number/index.ts +++ b/src/common/utils/number/index.ts @@ -6,7 +6,7 @@ NP.enableBoundaryChecking(false) export const numPrefix = (num: number | string) => { const parsedNum = parseFloat(num + '') - return parsedNum > 0 ? `+${num}` : num + return parsedNum > 0 ? `+${num}` : `${num}` } export const numAbbr = (num: number, decPlaces: number = 1) => From da2e7019ab28d8e205bc87357478218ec1469b98 Mon Sep 17 00:00:00 2001 From: robertu <4065233+robertu7@users.noreply.github.com> Date: Thu, 2 Nov 2023 21:22:21 +0800 Subject: [PATCH 04/62] feat(test): add tests for utils/detect --- src/common/utils/detect.test.ts | 31 ++++++++++++++++++++++ src/common/utils/{browser.ts => detect.ts} | 6 ----- src/common/utils/index.ts | 2 +- 3 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 src/common/utils/detect.test.ts rename src/common/utils/{browser.ts => detect.ts} (63%) diff --git a/src/common/utils/detect.test.ts b/src/common/utils/detect.test.ts new file mode 100644 index 0000000000..e2737bbc50 --- /dev/null +++ b/src/common/utils/detect.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest' + +import { getUserAgent, isMobile } from './detect' + +describe('utils/detect/getUserAgent', () => { + it('should return the user agent in lowercase', () => { + Object.defineProperty(window.navigator, 'userAgent', { + value: 'TestAgent', + configurable: true, + }) + expect(getUserAgent()).toBe('testagent') + }) +}) + +describe('utils/detect/isMobile', () => { + it('should return true for mobile user agents', () => { + Object.defineProperty(window.navigator, 'userAgent', { + value: 'iPhone', + configurable: true, + }) + expect(isMobile()).toBe(true) + }) + + it('should return false for non-mobile user agents', () => { + Object.defineProperty(window.navigator, 'userAgent', { + value: 'Windows NT', + configurable: true, + }) + expect(isMobile()).toBe(false) + }) +}) diff --git a/src/common/utils/browser.ts b/src/common/utils/detect.ts similarity index 63% rename from src/common/utils/browser.ts rename to src/common/utils/detect.ts index 41c461af62..63dd3af964 100644 --- a/src/common/utils/browser.ts +++ b/src/common/utils/detect.ts @@ -1,12 +1,6 @@ export const getUserAgent = () => ((navigator && navigator.userAgent) || '').toLowerCase() -export const isSafari = () => { - const userAgent = getUserAgent() - const match = userAgent.match(/version\/(\d+).+?safari/) - return match !== null -} - export const isMobile = () => { const userAgent = getUserAgent() return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index 64483765b5..b2b2922a3f 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -1,12 +1,12 @@ export * from './abis' export * from './analytics' export * from './audioPlayer' -export * from './browser' export * from './comment' export * from './connections' export * from './cookie' export * from './crypto' export * from './datetime' +export * from './detect' export * from './dom' export * from './form' export * from './globalId' From 39ac06b063cb917dbc709213d137107e33733bd6 Mon Sep 17 00:00:00 2001 From: robertu <4065233+robertu7@users.noreply.github.com> Date: Thu, 2 Nov 2023 21:26:43 +0800 Subject: [PATCH 05/62] feat(test): add tests for utils/dom --- src/common/utils/dom.test.ts | 29 +++++++++++++++++++++++++++++ src/common/utils/dom.ts | 24 ------------------------ 2 files changed, 29 insertions(+), 24 deletions(-) create mode 100644 src/common/utils/dom.test.ts diff --git a/src/common/utils/dom.test.ts b/src/common/utils/dom.test.ts new file mode 100644 index 0000000000..acb9bc194f --- /dev/null +++ b/src/common/utils/dom.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest' + +import { dom } from './dom' + +describe('utils/dom/getAttributes', () => { + it('should return an empty array if no matches are found', () => { + const result = dom.getAttributes('src', '
') + expect(result).toEqual([]) + }) + + it('should return an array of matches', () => { + const result = dom.getAttributes( + 'src', + '' + ) + expect(result).toEqual(['image1.jpg', 'image2.jpg']) + + const result2 = dom.getAttributes( + 'data-id', + '' + ) + expect(result2).toEqual(['123', '345']) + }) + + it('should ignore empty attributes', () => { + const result = dom.getAttributes('src', '') + expect(result).toEqual(['image.jpg']) + }) +}) diff --git a/src/common/utils/dom.ts b/src/common/utils/dom.ts index 0254a20a6a..e008f9a0d1 100644 --- a/src/common/utils/dom.ts +++ b/src/common/utils/dom.ts @@ -1,27 +1,6 @@ -// Select export const $ = (selector: string) => document.querySelector(selector) export const $$ = (selector: string) => document.querySelectorAll(selector) -// Size -const getWindowHeight = () => - window.innerHeight || - document.documentElement.clientHeight || - document.body.clientHeight -const getWindowWidth = () => - window.innerWidth || - document.documentElement.clientWidth || - document.body.clientWidth - -// Position -const offset = (el: HTMLElement) => { - const rect = el.getBoundingClientRect() - - return { - top: rect.top + document.body.scrollTop, - left: rect.left + document.body.scrollLeft, - } -} - const getAttributes = (name: string, str: string): string[] | [] => { const re = new RegExp(`${name}="(.*?)"`, 'g') const matches = [] @@ -36,8 +15,5 @@ const getAttributes = (name: string, str: string): string[] | [] => { export const dom = { $, $$, - getWindowHeight, - getWindowWidth, - offset, getAttributes, } From 02b13be48a95fd45543437e0550981e4e7f66c89 Mon Sep 17 00:00:00 2001 From: robertu <4065233+robertu7@users.noreply.github.com> Date: Fri, 3 Nov 2023 15:29:55 +0800 Subject: [PATCH 06/62] feat(test): add tests for utils/crpyto, utils/globalId, utils/locale and utils/pad --- src/common/utils/crypto.test.ts | 21 ++++++++ src/common/utils/crypto.ts | 3 ++ src/common/utils/globalId.test.ts | 19 +++++++ src/common/utils/index.ts | 5 +- src/common/utils/language.ts | 68 -------------------------- src/common/utils/locale.test.ts | 33 +++++++++++++ src/common/utils/locale.ts | 64 ++++++++++++++++++++++++ src/common/utils/pad.test.ts | 25 ++++++++++ src/common/utils/random.ts | 2 - src/common/utils/test.tsx | 2 +- src/common/utils/{time.ts => timer.ts} | 0 tests/mutateArticle.spec.ts | 2 +- tests/switchBetweenMultiUser.spec.ts | 2 +- 13 files changed, 170 insertions(+), 76 deletions(-) create mode 100644 src/common/utils/crypto.test.ts create mode 100644 src/common/utils/globalId.test.ts delete mode 100644 src/common/utils/language.ts create mode 100644 src/common/utils/locale.test.ts create mode 100644 src/common/utils/locale.ts create mode 100644 src/common/utils/pad.test.ts delete mode 100644 src/common/utils/random.ts rename src/common/utils/{time.ts => timer.ts} (100%) diff --git a/src/common/utils/crypto.test.ts b/src/common/utils/crypto.test.ts new file mode 100644 index 0000000000..0f533f16ab --- /dev/null +++ b/src/common/utils/crypto.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest' + +import { randomString } from './crypto' + +describe('utils/crypto/randomString', () => { + it('should generate a random string', () => { + const result = randomString() + const result2 = randomString() + expect(result).toHaveLength(9) + expect(result2).toHaveLength(9) + expect(result).not.toBe(result2) + }) + + it('should generate a random string with a custom length', () => { + const result = randomString(5) + const result2 = randomString(5) + expect(result).toHaveLength(5) + expect(result2).toHaveLength(5) + expect(result).not.toBe(result2) + }) +}) diff --git a/src/common/utils/crypto.ts b/src/common/utils/crypto.ts index 7606664651..72aa186bd6 100644 --- a/src/common/utils/crypto.ts +++ b/src/common/utils/crypto.ts @@ -11,3 +11,6 @@ export const generateChallenge = async (code_verifier: string) => { .replace(/\+/g, '-') .replace(/=/g, '') } + +export const randomString = (length = 9) => + Math.random().toString(36).substr(2, length) diff --git a/src/common/utils/globalId.test.ts b/src/common/utils/globalId.test.ts new file mode 100644 index 0000000000..bbf9c5b03e --- /dev/null +++ b/src/common/utils/globalId.test.ts @@ -0,0 +1,19 @@ +import { Base64 } from 'js-base64' +import { describe, expect, it } from 'vitest' + +import { fromGlobalId, toGlobalId } from './globalId' + +describe('utils/globalId/toGlobalId', () => { + it('should encode the type and id correctly', () => { + const result = toGlobalId({ type: 'User', id: '123' }) + expect(result).toBe(Base64.encodeURI('User:123')) + }) +}) + +describe('utils/globalId/fromGlobalId', () => { + it('should decode the globalId correctly', () => { + const globalId = Base64.encodeURI('User:123') + const result = fromGlobalId(globalId) + expect(result).toEqual({ type: 'User', id: '123' }) + }) +}) diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index b2b2922a3f..e7f35eac9d 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -11,18 +11,17 @@ export * from './dom' export * from './form' export * from './globalId' export * from './iscnLink' -export * from './language' +export * from './locale' export * from './number' export * from './oauth' export * from './pad' export * from './payment' -export * from './random' export * from './response' export * from './route' export * from './search' export * from './storage' export * from './text' -export * from './time' +export * from './timer' export * from './translate' export * from './url' export * from './validator' diff --git a/src/common/utils/language.ts b/src/common/utils/language.ts deleted file mode 100644 index 39abf233eb..0000000000 --- a/src/common/utils/language.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { UserLanguage } from '~/gql/graphql' - -export const toUserLanguage = (lang: string) => { - lang = lang.toLowerCase() - - // zh_hans - if (['zh-cn', 'zh-hans', 'zh_hans'].indexOf(lang) >= 0) { - return UserLanguage.ZhHans - } - - // zh_hant - if (['zh', 'zh-tw', 'zh-hk', 'zh-hant', 'zh_hant'].indexOf(lang) >= 0) { - return UserLanguage.ZhHant - } - - // en - if (['en', 'en-us', 'en-au', 'en-za', 'en-gb'].indexOf(lang) >= 0) { - return UserLanguage.En - } - - return '' -} - -export const toLocale = (lang: string) => { - lang = lang.toLowerCase() - - // zh_hans - if (['zh-cn', 'zh_cn', 'zh-hans', 'zh_hans'].indexOf(lang) >= 0) { - return 'zh-Hans' - } - - // zh_hant - if ( - ['zh', 'zh_tw', 'zh-tw', 'zh_hk', 'zh-hk', 'zh-hant', 'zh_hant'].indexOf( - lang - ) >= 0 - ) { - return 'zh-Hant' - } - - // en - if (['en', 'en-us', 'en-au', 'en-za', 'en-gb'].indexOf(lang) >= 0) { - return 'en' - } - - return '' -} - -export const toOGLanguage = (lang: string) => { - lang = lang.toLowerCase() - - // zh_hans - if (['zh-cn', 'zh-hans', 'zh_hans'].indexOf(lang) >= 0) { - return 'zh_CN' - } - - // zh_hant - if (['zh', 'zh-tw', 'zh-hk', 'zh-hant', 'zh_hant'].indexOf(lang) >= 0) { - return 'zh_TW' - } - - // en - if (['en', 'en-us', 'en-au', 'en-za', 'en-gb'].indexOf(lang) >= 0) { - return 'en' - } - - return '' -} diff --git a/src/common/utils/locale.test.ts b/src/common/utils/locale.test.ts new file mode 100644 index 0000000000..21dc4d5e4b --- /dev/null +++ b/src/common/utils/locale.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest' + +import { toLocale, toOGLanguage, toUserLanguage } from './locale' + +describe('utils/locale/toUserLanguage', () => { + it('should convert to zh_Hans', () => { + expect(toUserLanguage('zh-cn')).toBe('zh_hans') + expect(toUserLanguage('zh_cn')).toBe('zh_hans') + expect(toUserLanguage('zh-hans')).toBe('zh_hans') + expect(toUserLanguage('zh_hans')).toBe('zh_hans') + expect(toUserLanguage('zh-CN')).toBe('zh_hans') + }) +}) + +describe('utils/locale/toLocale', () => { + it('should convert to zh-Hans', () => { + expect(toLocale('zh-cn')).toBe('zh-Hans') + expect(toLocale('zh_cn')).toBe('zh-Hans') + expect(toLocale('zh-hans')).toBe('zh-Hans') + expect(toLocale('zh_hans')).toBe('zh-Hans') + expect(toLocale('zh-CN')).toBe('zh-Hans') + }) +}) + +describe('utils/locale/toOGLanguage', () => { + it('should convert to zh_CN', () => { + expect(toOGLanguage('zh-cn')).toBe('zh_CN') + expect(toOGLanguage('zh_cn')).toBe('zh_CN') + expect(toOGLanguage('zh-hans')).toBe('zh_CN') + expect(toOGLanguage('zh_hans')).toBe('zh_CN') + expect(toOGLanguage('zh-CN')).toBe('zh_CN') + }) +}) diff --git a/src/common/utils/locale.ts b/src/common/utils/locale.ts new file mode 100644 index 0000000000..f5a11350bc --- /dev/null +++ b/src/common/utils/locale.ts @@ -0,0 +1,64 @@ +import { UserLanguage } from '~/gql/graphql' + +export const toUserLanguage = (lang: string) => { + lang = lang.toLowerCase().replace(/-/g, '_') + + // zh_hans + if (['zh_cn', 'zh_hans'].indexOf(lang) >= 0) { + return UserLanguage.ZhHans + } + + // zh_hant + if (['zh', 'zh_tw', 'zh_hk', 'zh_hant'].indexOf(lang) >= 0) { + return UserLanguage.ZhHant + } + + // en + if (lang.startsWith('en')) { + return UserLanguage.En + } + + return '' +} + +export const toLocale = (lang: string) => { + lang = lang.toLowerCase().replace(/-/g, '_') + + // zh_hans + if (['zh_cn', 'zh_hans'].indexOf(lang) >= 0) { + return 'zh-Hans' + } + + // zh_hant + if (['zh', 'zh_tw', 'zh_hk', 'zh_hant'].indexOf(lang) >= 0) { + return 'zh-Hant' + } + + // en + if (lang.startsWith('en')) { + return 'en' + } + + return '' +} + +export const toOGLanguage = (lang: string) => { + lang = lang.toLowerCase().replace(/-/g, '_') + + // zh_hans + if (['zh_cn', 'zh_hans'].indexOf(lang) >= 0) { + return 'zh_CN' + } + + // zh_hant + if (['zh', 'zh_tw', 'zh_hk', 'zh_hant'].indexOf(lang) >= 0) { + return 'zh_TW' + } + + // en + if (lang.startsWith('en')) { + return 'en' + } + + return '' +} diff --git a/src/common/utils/pad.test.ts b/src/common/utils/pad.test.ts new file mode 100644 index 0000000000..0d203c39a3 --- /dev/null +++ b/src/common/utils/pad.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest' + +import { leftPad } from './pad' + +describe('utils/pad', () => { + it('should pad a string on the left', () => { + const result = leftPad('abc', 5) + expect(result).toBe(' abc') + }) + + it('should not pad a string if its length is equal to the target length', () => { + const result = leftPad('abcd', 4) + expect(result).toBe('abcd') + }) + + it('should not pad a string if its length is greater than the target length', () => { + const result = leftPad('abcde', 4) + expect(result).toBe('abcde') + }) + + it('should pad a string with a custom character', () => { + const result = leftPad('abc', 5, '-') + expect(result).toBe('--abc') + }) +}) diff --git a/src/common/utils/random.ts b/src/common/utils/random.ts deleted file mode 100644 index 855fa87c23..0000000000 --- a/src/common/utils/random.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const randomString = (length = 9) => - Math.random().toString(36).substr(2, length) diff --git a/src/common/utils/test.tsx b/src/common/utils/test.tsx index 91b2aeec4e..c2ee81890e 100644 --- a/src/common/utils/test.tsx +++ b/src/common/utils/test.tsx @@ -27,7 +27,7 @@ import GlobalDialogs from '~/components/GlobalDialogs' import { UserLanguage } from '~/gql/graphql' import { MOCK_USER } from '~/stories/mocks' -import { toLocale } from './language' +import { toLocale } from './locale' const TranslationsProvider = ({ children }: { children: React.ReactNode }) => { const { lang } = useContext(LanguageContext) diff --git a/src/common/utils/time.ts b/src/common/utils/timer.ts similarity index 100% rename from src/common/utils/time.ts rename to src/common/utils/timer.ts diff --git a/tests/mutateArticle.spec.ts b/tests/mutateArticle.spec.ts index 881d51dfed..5cd9a8448e 100644 --- a/tests/mutateArticle.spec.ts +++ b/tests/mutateArticle.spec.ts @@ -2,8 +2,8 @@ import { expect, test } from '@playwright/test' import _random from 'lodash/random' import { TEST_ID } from '~/common/enums' +import { sleep } from '~/common/utils' import { stripSpaces } from '~/common/utils/text' -import { sleep } from '~/common/utils/time' import { publishDraft } from './common' import { diff --git a/tests/switchBetweenMultiUser.spec.ts b/tests/switchBetweenMultiUser.spec.ts index 6d096a4c97..dcba589e7e 100644 --- a/tests/switchBetweenMultiUser.spec.ts +++ b/tests/switchBetweenMultiUser.spec.ts @@ -1,7 +1,7 @@ import { expect, test } from '@playwright/test' import { TEST_ID } from '~/common/enums' -import { sleep } from '~/common/utils/time' +import { sleep } from '~/common/utils' import { ArticleDetailPage, From 107e7d1fc713c5c91743cb71836e08e0d073adcd Mon Sep 17 00:00:00 2001 From: robertu <4065233+robertu7@users.noreply.github.com> Date: Sat, 4 Nov 2023 10:22:47 +0800 Subject: [PATCH 07/62] feat(test): add tests for utils/storage --- src/common/utils/storage.test.ts | 35 ++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/common/utils/storage.test.ts diff --git a/src/common/utils/storage.test.ts b/src/common/utils/storage.test.ts new file mode 100644 index 0000000000..a9bb082140 --- /dev/null +++ b/src/common/utils/storage.test.ts @@ -0,0 +1,35 @@ +import { beforeEach, describe, expect, it } from 'vitest' + +import { sessionStorage as sssstorage, storage } from './storage' + +describe('utils/storage', () => { + beforeEach(() => { + localStorage.clear() + }) + + it('should set, get and remove the item in localStorage', () => { + const key = 'testKey' + const value = { data: 'testData' } + + storage.set(key, value) + expect(localStorage.getItem(key)).toBe(JSON.stringify(value)) + expect(storage.get(key)).toEqual(value) + + storage.remove(key) + expect(localStorage.getItem(key)).toBe(null) + expect(storage.get(key)).toEqual(null) + }) + + it('should set, get and remove the item in sessionStorage', () => { + const key = 'testKey' + const value = { data: 'testData' } + + sssstorage.set(key, value) + expect(sessionStorage.getItem(key)).toBe(JSON.stringify(value)) + expect(sssstorage.get(key)).toEqual(value) + + sssstorage.remove(key) + expect(sessionStorage.getItem(key)).toBe(null) + expect(sssstorage.get(key)).toEqual(null) + }) +}) From aef090f448ed66fd7dac940f4f70b68de4054ada Mon Sep 17 00:00:00 2001 From: robertu <4065233+robertu7@users.noreply.github.com> Date: Sat, 4 Nov 2023 10:50:09 +0800 Subject: [PATCH 08/62] feat(test): add tests for utils/url --- src/common/utils/url.test.ts | 43 ++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/common/utils/url.test.ts diff --git a/src/common/utils/url.test.ts b/src/common/utils/url.test.ts new file mode 100644 index 0000000000..ba9e90c10b --- /dev/null +++ b/src/common/utils/url.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest' + +import { isUrl } from './url' + +describe('utils/url', () => { + it('should return true for valid URLs', () => { + expect(isUrl('example.com')).toBe(true) + expect(isUrl('http://example.com')).toBe(true) + expect(isUrl('https://example.com')).toBe(true) + expect(isUrl('https://www.example.com')).toBe(true) + expect(isUrl('https://example.com/path')).toBe(true) + expect(isUrl('https://example.com/path?query=param')).toBe(true) + expect( + isUrl( + 'https://example.com/@userName/10010-%E4%BA%94-bafybeidlgjmkj6tgtmeyeor' + ) + ).toBe(true) + expect(isUrl('https://example.com/#hash')).toBe(true) + expect(isUrl('https://example.com/?query=param#hash')).toBe(true) + expect(isUrl('http://example.com:8080')).toBe(true) + expect(isUrl('http://example.com:8080/path')).toBe(true) + expect(isUrl('http://example.com:8080/path?query=param')).toBe(true) + expect(isUrl('http://example.com:8080/#hash')).toBe(true) + + // unusual but valid + expect(isUrl('http:/example.com')).toBe(false) + expect(isUrl('http://example')).toBe(false) + expect(isUrl('https://example')).toBe(false) + expect(isUrl('http://.com')).toBe(false) + expect(isUrl('https://example.com:')).toBe(false) + }) + + it('should return false for invalid URLs', () => { + expect(isUrl('example')).toBe(false) + expect(isUrl('http://example.com:port')).toBe(false) + expect(isUrl('https://example.com:port')).toBe(false) + expect(isUrl('https://example.com:port/path')).toBe(false) + expect(isUrl('https://example.com:port/path?query=param')).toBe(false) + expect(isUrl('https://example.com:port/#hash')).toBe(false) + expect(isUrl('https://example.com:port/?query=param#hash')).toBe(false) + expect(isUrl('http://example.com:port:8080')).toBe(false) + }) +}) From 3bd4259df7d77523906a9f08f59d9533bbc87109 Mon Sep 17 00:00:00 2001 From: robertu <4065233+robertu7@users.noreply.github.com> Date: Sat, 4 Nov 2023 12:13:45 +0800 Subject: [PATCH 09/62] feat(test): add tests for utils/url and utils/validator --- src/common/utils/url.test.ts | 190 +++++++++++++++++- src/common/utils/url.ts | 47 +---- src/common/utils/validator.test.ts | 64 ++++++ .../CollectionArticles/index.tsx | 4 +- 4 files changed, 258 insertions(+), 47 deletions(-) create mode 100644 src/common/utils/validator.test.ts diff --git a/src/common/utils/url.test.ts b/src/common/utils/url.test.ts index ba9e90c10b..9d4edd86b0 100644 --- a/src/common/utils/url.test.ts +++ b/src/common/utils/url.test.ts @@ -1,8 +1,14 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' -import { isUrl } from './url' +import { + isUrl, + parseSorter, + parseURL, + stringifySorter, + toSizedImageURL, +} from './url' -describe('utils/url', () => { +describe('utils/url/isUrl', () => { it('should return true for valid URLs', () => { expect(isUrl('example.com')).toBe(true) expect(isUrl('http://example.com')).toBe(true) @@ -21,17 +27,17 @@ describe('utils/url', () => { expect(isUrl('http://example.com:8080/path')).toBe(true) expect(isUrl('http://example.com:8080/path?query=param')).toBe(true) expect(isUrl('http://example.com:8080/#hash')).toBe(true) + expect(isUrl('https://example.com:')).toBe(true) + }) - // unusual but valid + it('should return false for invalid URLs', () => { + expect(isUrl('example')).toBe(false) + expect(isUrl('mailto:alice@example.com')).toBe(false) + expect(isUrl('tel:88888888')).toBe(false) expect(isUrl('http:/example.com')).toBe(false) expect(isUrl('http://example')).toBe(false) expect(isUrl('https://example')).toBe(false) expect(isUrl('http://.com')).toBe(false) - expect(isUrl('https://example.com:')).toBe(false) - }) - - it('should return false for invalid URLs', () => { - expect(isUrl('example')).toBe(false) expect(isUrl('http://example.com:port')).toBe(false) expect(isUrl('https://example.com:port')).toBe(false) expect(isUrl('https://example.com:port/path')).toBe(false) @@ -41,3 +47,169 @@ describe('utils/url', () => { expect(isUrl('http://example.com:port:8080')).toBe(false) }) }) + +describe('utils/url/parseURL', () => { + it('should parse URL', () => { + expect(parseURL('https://example.com:8080/path?query=param#hash')).toEqual({ + protocol: 'https:', + host: 'example.com:8080', + hostname: 'example.com', + port: '8080', + pathname: '/path', + search: '?query=param', + hash: '#hash', + }) + + expect(parseURL('http://example.com')).toEqual({ + protocol: 'http:', + host: 'example.com', + hostname: 'example.com', + port: '', + pathname: '/', + search: '', + hash: '', + }) + + expect( + parseURL( + 'https://example.com/@userName/10010-%E4%BA%94-bafybeidlgjmkj6tgtmeyeor' + ) + ).toEqual({ + protocol: 'https:', + host: 'example.com', + hostname: 'example.com', + port: '', + pathname: '/@userName/10010-%E4%BA%94-bafybeidlgjmkj6tgtmeyeor', + search: '', + hash: '', + }) + + // uncommon URLs + expect(parseURL('example.com').hostname).not.toBe('example.com') + expect(parseURL('').hostname).not.toBe('example.com') + }) +}) + +describe('utils/url/parseSorter', () => { + it('should parse sorter', () => { + expect(parseSorter('')).toEqual({}) + expect(parseSorter('date:')).toEqual({ date: '' }) + expect(parseSorter('date')).toEqual({ date: '' }) + expect(parseSorter('date:asc')).toEqual({ date: 'asc' }) + expect(parseSorter('date: asc')).toEqual({ date: 'asc' }) + expect(parseSorter('date:asc,')).toEqual({ date: 'asc' }) + expect(parseSorter('date:asc, ')).toEqual({ date: 'asc' }) + expect(parseSorter('date:asc,like:desc')).toEqual({ + date: 'asc', + like: 'desc', + }) + expect(parseSorter('date:asc, like:desc')).toEqual({ + date: 'asc', + like: 'desc', + }) + }) +}) + +describe('utils/url/stringifySorter', () => { + it('should stringify sorter', () => { + expect(stringifySorter({})).toBe('') + expect(stringifySorter({ date: '' })).toBe('date:') + expect(stringifySorter({ date: 'asc' })).toBe('date:asc') + expect(stringifySorter({ date: 'asc', like: 'desc' })).toBe( + 'date:asc,like:desc' + ) + expect(stringifySorter({ date: 'asc', like: 'desc', comment: '' })).toBe( + 'date:asc,like:desc,comment:' + ) + }) +}) + +describe('utils/url/toSizedImageURL', () => { + it('should return image URL with asset domain', () => { + const NEXT_PUBLIC_CF_IMAGE_URL = 'https://imagedelivery.net/abc/prod' + vi.stubEnv('NEXT_PUBLIC_CF_IMAGE_URL', NEXT_PUBLIC_CF_IMAGE_URL) + + // with `/public` suffix + expect( + toSizedImageURL({ + url: 'https://imagedelivery.net/abc/prod/image.jpeg/public', + width: 72, + }) + ).toBe( + 'https://imagedelivery.net/abc/prod/image.jpeg/w=72,h=288,fit=scale-down' + ) + + // width + expect( + toSizedImageURL({ + url: 'https://imagedelivery.net/abc/prod/image.jpeg', + width: 72, + }) + ).toBe( + 'https://imagedelivery.net/abc/prod/image.jpeg/w=72,h=288,fit=scale-down' + ) + + // with width and height + expect( + toSizedImageURL({ + url: 'https://imagedelivery.net/abc/prod/image.jpeg', + width: 72, + height: 72, + }) + ).toBe('https://imagedelivery.net/abc/prod/image.jpeg/w=72,h=72,fit=crop') + + // with width and height and ext + expect( + toSizedImageURL({ + url: 'https://imagedelivery.net/abc/prod/image.jpeg', + width: 72, + height: 72, + ext: 'webp', + }) + ).toBe('https://imagedelivery.net/abc/prod/image.webp/w=72,h=72,fit=crop') + + // with width and height and disableAnimation + expect( + toSizedImageURL({ + url: 'https://imagedelivery.net/abc/prod/image.gif', + width: 72, + height: 72, + disableAnimation: true, + }) + ).toBe( + 'https://imagedelivery.net/abc/prod/image.gif/w=72,h=72,fit=crop,anim=false' + ) + }) + + it('should return image URL with thrid-party domain', () => { + const NEXT_PUBLIC_CF_IMAGE_URL = 'https://imagedelivery.net/abc/prod' + vi.stubEnv('NEXT_PUBLIC_CF_IMAGE_URL', NEXT_PUBLIC_CF_IMAGE_URL) + + expect( + toSizedImageURL({ + url: 'https://example.com/abc/prod/image.jpeg/public', + width: 72, + }) + ).toBe('https://example.com/abc/prod/image.jpeg/public') + }) + + it('should return image URL w/o asset domain', () => { + // external domain + expect( + toSizedImageURL({ + url: 'https://example.com/abc/prod/image.jpeg/public', + width: 72, + }) + ).toBe('https://example.com/abc/prod/image.jpeg/public') + + // legacy domain + expect( + toSizedImageURL({ + url: 'https://assets.matters.news/cover/63049798-ea19-4ba1-9325-d93ae4cc4857.jpeg', + width: 72, + }) + ).toBe( + 'https://assets.matters.news/cover/63049798-ea19-4ba1-9325-d93ae4cc4857.jpeg' + ) + }) +}) diff --git a/src/common/utils/url.ts b/src/common/utils/url.ts index 70549efb45..fc14b5644f 100644 --- a/src/common/utils/url.ts +++ b/src/common/utils/url.ts @@ -1,11 +1,6 @@ -import { URL_COLLECTION_DETAIL } from '../enums' - -const pattern = /^(:?\/\/|https?:\/\/)?([^/]*@)?(.+?)(:\d{2,5})?([/?].*)?$/ +export { default as isUrl } from 'validator/lib/isUrl' -export const extractDomain = (url: string) => { - const parts = url.match(pattern) || [] - return parts[3] -} +import { URL_COLLECTION_DETAIL } from '../enums' export const parseURL = (url: string) => { const parser = document.createElement('a') @@ -36,7 +31,7 @@ interface ToSizedImageURLProps { disableAnimation?: boolean } -export const changeExt = ({ key, ext }: { key: string; ext?: 'webp' }) => { +const changeExt = ({ key, ext }: { key: string; ext?: 'webp' }) => { const list = key.split('.') const hasExt = list.length > 1 const newExt = ext || list.slice(-1)[0] || '' @@ -89,41 +84,19 @@ export const toSizedImageURL = ({ return assetDomain + extedUrl + '/' + postfix } -export const isUrl = (key: string) => { - let valid = false - - try { - valid = Boolean(new URL(key)) - } catch (e) { - // do nothing - } - - if (valid) { - return valid - } - - // fallback to match url w/o protocol - const pattern = new RegExp( - '^([a-zA-Z]+:\\/\\/)?' + // protocol - '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name - '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR IP (v4) address - '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path - '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string - '(\\#[-a-z\\d_]*)?$', // fragment locator - 'i' - ) - return pattern.test(key) -} - export const parseSorter = (sorterStr: string) => { - const sorter: any = {} + const sorter: { [key: string]: string } = {} if (sorterStr === '') { return sorter } const sorters = sorterStr.split(URL_COLLECTION_DETAIL.SORTER_SEPARATOR) sorters.map((s) => { - const [key, value] = s.split(URL_COLLECTION_DETAIL.SORTER_TYPE_SEPARATOR) - sorter[key] = value + let [key, value] = s.split(URL_COLLECTION_DETAIL.SORTER_TYPE_SEPARATOR) + key = (key || '').trim() + value = (value || '').trim() + if (key) { + sorter[key] = value + } }) return sorter } diff --git a/src/common/utils/validator.test.ts b/src/common/utils/validator.test.ts new file mode 100644 index 0000000000..4d915bdf9b --- /dev/null +++ b/src/common/utils/validator.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from 'vitest' + +import { + isValidEmail, + isValidPassword, + isValidPaymentPassword, + isValidPaymentPointer, +} from './validator' + +describe('utils/validator/isValidEmail', () => { + it('should return true for valid emails', () => { + const allowPlusSign = false + expect(isValidEmail('alice@example.com', { allowPlusSign })).toBe(true) + expect(isValidEmail('alice@example.com', { allowPlusSign })).toBe(true) + }) +}) + +describe('utils/validator/isValidPassword', () => { + it('should return true for valid passwords', () => { + expect(isValidPassword('12345678')).toBe(true) + expect(isValidPassword('abcdefgh')).toBe(true) + expect(isValidPassword('ABCDEFGH')).toBe(true) + expect(isValidPassword('!@#$%^&*')).toBe(true) + expect(isValidPassword('123abcDEF%^&*')).toBe(true) + expect(isValidPassword(' ')).toBe(true) + }) + + it('should return false for invalid passwords', () => { + // length + expect(isValidPassword('12345')).toBe(false) + + // non-ASCII + expect(isValidPassword('你好世界你好世界')).toBe(false) + }) +}) + +describe('utils/validator/isValidPaymentPassword', () => { + it('should return true for valid payment passwords', () => { + expect(isValidPaymentPassword('123456')).toBe(true) + }) + + it('should return false for invalid payment passwords', () => { + expect(isValidPaymentPassword('12345')).toBe(false) + expect(isValidPaymentPassword('1234567')).toBe(false) + expect(isValidPassword('abcdef')).toBe(false) + expect(isValidPassword('ABCDEF')).toBe(false) + expect(isValidPassword('!@#$%^')).toBe(false) + expect(isValidPassword('1aEF%*')).toBe(false) + expect(isValidPassword(' ')).toBe(false) + expect(isValidPassword('你好世界你你')).toBe(false) + }) +}) + +describe('utils/validator/isValidPaymentPointer', () => { + it('should return true for valid payment pointers', () => { + expect(isValidPaymentPointer('$example.com')).toBe(true) + expect(isValidPaymentPointer('$www.example.com')).toBe(true) + }) + + it('should return false for invalid payment pointers', () => { + expect(isValidPaymentPointer('example.com')).toBe(false) + expect(isValidPaymentPointer('www.example.com')).toBe(false) + }) +}) diff --git a/src/views/User/CollectionDetail/CollectionArticles/index.tsx b/src/views/User/CollectionDetail/CollectionArticles/index.tsx index 28ed31af5f..d964460f0a 100644 --- a/src/views/User/CollectionDetail/CollectionArticles/index.tsx +++ b/src/views/User/CollectionDetail/CollectionArticles/index.tsx @@ -43,7 +43,9 @@ const CollectionArticles = ({ collection }: CollectionArticlesProps) => { let sorter = parseSorter(getQuery(URL_COLLECTION_DETAIL.SORTER_KEY)) - let sorterSequence = sorter[URL_COLLECTION_DETAIL.SORTER_SEQUENCE.key] + let sorterSequence = sorter[ + URL_COLLECTION_DETAIL.SORTER_SEQUENCE.key + ] as SorterSequenceType if ( sorterSequence !== URL_COLLECTION_DETAIL.SORTER_SEQUENCE.value.DSC && From feab01f2117a47318b5ec3a96f59b932ac5b33b2 Mon Sep 17 00:00:00 2001 From: robertu <4065233+robertu7@users.noreply.github.com> Date: Sun, 5 Nov 2023 14:07:58 +0800 Subject: [PATCH 10/62] feat(test): add test for utils/timer --- src/common/utils/index.ts | 1 - src/common/utils/route.ts | 7 ++++ src/common/utils/search.ts | 6 --- src/common/utils/timer.test.ts | 54 ++++++++++++++++++++++++++ src/common/utils/timer.ts | 10 ----- src/components/Popper/Tooltip.test.tsx | 13 ++----- src/components/Toast/Toast.test.tsx | 3 +- 7 files changed, 66 insertions(+), 28 deletions(-) delete mode 100644 src/common/utils/search.ts create mode 100644 src/common/utils/timer.test.ts diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index e7f35eac9d..2c61e220c1 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -18,7 +18,6 @@ export * from './pad' export * from './payment' export * from './response' export * from './route' -export * from './search' export * from './storage' export * from './text' export * from './timer' diff --git a/src/common/utils/route.ts b/src/common/utils/route.ts index 7f49a0e042..b641df903e 100644 --- a/src/common/utils/route.ts +++ b/src/common/utils/route.ts @@ -395,3 +395,10 @@ export const captureClicks = (e: React.MouseEvent) => { Router.push(el.href) } } + +export type SearchType = 'article' | 'user' | 'tag' | undefined + +export const getSearchType = (value: string): SearchType => { + const types = ['article', 'user', 'tag'] + return types.includes(value) ? (value as SearchType) : undefined +} diff --git a/src/common/utils/search.ts b/src/common/utils/search.ts deleted file mode 100644 index 7b6a901c78..0000000000 --- a/src/common/utils/search.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type SearchType = 'article' | 'user' | 'tag' | undefined - -export const getSearchType = (value: string): SearchType => { - const types = ['article', 'user', 'tag'] - return types.includes(value) ? (value as SearchType) : undefined -} diff --git a/src/common/utils/timer.test.ts b/src/common/utils/timer.test.ts new file mode 100644 index 0000000000..b23e4dc491 --- /dev/null +++ b/src/common/utils/timer.test.ts @@ -0,0 +1,54 @@ +import { act } from '@testing-library/react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { deferTry, sleep } from './timer' + +beforeEach(() => { + // Tests should run in serial for improved isolation + // To prevent collision with global state, reset all toasts for each test + vi.useFakeTimers() +}) + +afterEach(() => { + act(() => { + vi.runAllTimers() + vi.useRealTimers() + }) +}) + +describe('utils/timer/deferTry', () => { + it('should retry the function if it throws an error', async () => { + const fn = vi.fn(() => { + throw new Error() + }) + + const tries = 3 + const defer = 1000 + deferTry(fn, tries, defer) + + vi.advanceTimersByTime(defer * tries - 1) + expect(fn).toHaveBeenCalledTimes(tries) + }) + + it('should not retry the function if it does not throw an error', () => { + const fn = vi.fn() + + const tries = 3 + const defer = 1000 + deferTry(fn, tries, defer) + + expect(fn).toHaveBeenCalledTimes(1) + }) +}) + +describe('utils/timer/sleep', () => { + it('should resolve after the specified time', async () => { + const fn = vi.fn() + const duration = 1000 + sleep(duration).then(() => { + fn() + }) + await vi.advanceTimersByTimeAsync(duration) + expect(fn).toHaveBeenCalled() + }) +}) diff --git a/src/common/utils/timer.ts b/src/common/utils/timer.ts index 339ce57d89..5521d7e612 100644 --- a/src/common/utils/timer.ts +++ b/src/common/utils/timer.ts @@ -3,7 +3,6 @@ export const deferTry = (fn: () => any, timesLeft = 10, defer = 2000) => { try { fn() } catch (err) { - console.log(err) setTimeout(() => deferTry(fn, timesLeft - 1, defer), defer) } } @@ -15,12 +14,3 @@ export const sleep = async (ms: number) => { setTimeout(resolve, ms) }) } - -export const timeout = (ms: number, promise: any) => { - return new Promise((resolve, reject) => { - setTimeout(() => { - reject(new Error('timeout')) - }, ms) - promise.then(resolve, reject) - }) -} diff --git a/src/components/Popper/Tooltip.test.tsx b/src/components/Popper/Tooltip.test.tsx index d072d45ebd..ebe345f5ac 100644 --- a/src/components/Popper/Tooltip.test.tsx +++ b/src/components/Popper/Tooltip.test.tsx @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest' -import { act, fireEvent, render, screen } from '~/common/utils/test' +import { fireEvent, render, screen } from '~/common/utils/test' import { Tooltip } from '~/components' describe('', () => { @@ -57,16 +57,11 @@ describe('', () => { expect(screen.queryByText(content)).not.toBeInTheDocument() // content should be shown after delay - act(() => { - vi.advanceTimersByTime(delay) - }) + vi.advanceTimersByTime(delay) const $tooltip = screen.getByText(content) expect($tooltip).toBeInTheDocument() - act(() => { - vi.runAllTimers() - vi.useRealTimers() - // done(); - }) + vi.runAllTimers() + vi.useRealTimers() }) }) diff --git a/src/components/Toast/Toast.test.tsx b/src/components/Toast/Toast.test.tsx index 843b1fbda2..9a6dad3bf0 100644 --- a/src/components/Toast/Toast.test.tsx +++ b/src/components/Toast/Toast.test.tsx @@ -9,11 +9,10 @@ beforeEach(() => { vi.useFakeTimers() }) -afterEach((done) => { +afterEach(() => { act(() => { vi.runAllTimers() vi.useRealTimers() - // done(); }) }) From 3f6f2aec3f8b8db29b83d6a932df917250395a62 Mon Sep 17 00:00:00 2001 From: robertu <4065233+robertu7@users.noreply.github.com> Date: Sun, 5 Nov 2023 18:39:57 +0800 Subject: [PATCH 11/62] feat(test): add tests for utils/route/toPath and refactor --- src/common/utils/index.ts | 1 - src/common/utils/iscnLink.ts | 10 - src/common/utils/route.test.ts | 310 ++++++++++++++++++ src/common/utils/route.ts | 177 +++++----- .../Dialogs/FingerprintDialog/Content.tsx | 12 +- src/stories/mocks/index.ts | 18 +- src/views/ArticleDetail/index.tsx | 4 +- 7 files changed, 420 insertions(+), 112 deletions(-) delete mode 100644 src/common/utils/iscnLink.ts create mode 100644 src/common/utils/route.test.ts diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index 2c61e220c1..f2fb805177 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -10,7 +10,6 @@ export * from './detect' export * from './dom' export * from './form' export * from './globalId' -export * from './iscnLink' export * from './locale' export * from './number' export * from './oauth' diff --git a/src/common/utils/iscnLink.ts b/src/common/utils/iscnLink.ts deleted file mode 100644 index e357409b7f..0000000000 --- a/src/common/utils/iscnLink.ts +++ /dev/null @@ -1,10 +0,0 @@ -// e.g. https://app.like.co/view/iscn:%2F%2Flikecoin-chain%2FbZs7dmhEk0voCV6vI_eWaHGD-cY32z6zt1scgzV9DVI%2F1 - -const isProd = process.env.NEXT_PUBLIC_RUNTIME_ENV === 'production' -const iscnLinkPrefix = isProd - ? 'https://app.like.co/view' - : 'https://app.rinkeby.like.co/view' - -export function iscnLinkUrl(iscnId: string) { - return `${iscnLinkPrefix}/${encodeURIComponent(iscnId)}` -} diff --git a/src/common/utils/route.test.ts b/src/common/utils/route.test.ts new file mode 100644 index 0000000000..f5f6eea674 --- /dev/null +++ b/src/common/utils/route.test.ts @@ -0,0 +1,310 @@ +import { describe, expect, it } from 'vitest' + +import { + MOCK_ARTILCE, + MOCK_CIRCLE, + MOCK_COLLECTION, + MOCK_COMMENT, + MOCK_DRAFT, + MOCK_TAG, + MOCK_USER, +} from '~/stories/mocks' + +import { fromGlobalId } from './globalId' +import { + // appendTarget, + // getEncodedCurrent, + getSearchType, + // getTarget, + // redirectToHomePage, + // redirectToLogin, + // redirectToTarget, + toPath, +} from './route' + +describe('utils/route/toPath', () => { + describe('page: articleDetail', () => { + it('should return the correct path', () => { + const { href } = toPath({ page: 'articleDetail', article: MOCK_ARTILCE }) + // /@matty/1-slug-Qmaisz6NMhDB51cCvNWa1GMS7LU1pAxdF4Ld6Ft9kZEP2a + expect(href).toBe( + `/@${MOCK_ARTILCE.author.userName}/${ + fromGlobalId(MOCK_ARTILCE.id).id + }-${MOCK_ARTILCE.slug}-${MOCK_ARTILCE.mediaHash}` + ) + }) + + it('should return the correct path when the article id is not a valid global id', () => { + const { href } = toPath({ + page: 'articleDetail', + article: { + ...MOCK_ARTILCE, + id: 'invalidId', + }, + }) + // /@matty/slug-Qmaisz6NMhDB51cCvNWa1GMS7LU1pAxdF4Ld6Ft9kZEP2a + expect(href).toBe( + `/@${MOCK_ARTILCE.author.userName}/${MOCK_ARTILCE.slug}-${MOCK_ARTILCE.mediaHash}` + ) + }) + + it('should return the correct path when the article id is not provided', () => { + const { href } = toPath({ + page: 'articleDetail', + article: { + ...MOCK_ARTILCE, + id: undefined, + }, + }) + // /@matty/slug-Qmaisz6NMhDB51cCvNWa1GMS7LU1pAxdF4Ld6Ft9kZEP2a + expect(href).toBe( + `/@${MOCK_ARTILCE.author.userName}/${MOCK_ARTILCE.slug}-${MOCK_ARTILCE.mediaHash}` + ) + }) + + it('should return the correct path with utm paramaters', () => { + const { href } = toPath({ + page: 'articleDetail', + article: MOCK_ARTILCE, + utm_source: 'test-source', + utm_campaign: 'test-campaign', + }) + expect(href).toBe( + `/@${MOCK_ARTILCE.author.userName}/${ + fromGlobalId(MOCK_ARTILCE.id).id + }-${MOCK_ARTILCE.slug}-${ + MOCK_ARTILCE.mediaHash + }?utm_source=test-source&utm_campaign=test-campaign` + ) + }) + }) + + describe('page: circleDetail', () => { + it('should return the correct path', () => { + const { href } = toPath({ page: 'circleDetail', circle: MOCK_CIRCLE }) + expect(href).toBe(`/~${MOCK_CIRCLE.name}`) + }) + }) + + describe('page: circleDiscussion', () => { + it('should return the correct path', () => { + const { href } = toPath({ + page: 'circleDiscussion', + circle: MOCK_CIRCLE, + }) + expect(href).toBe(`/~${MOCK_CIRCLE.name}/discussion`) + }) + }) + + describe('page: circleBroadcast', () => { + it('should return the correct path', () => { + const { href } = toPath({ + page: 'circleBroadcast', + circle: MOCK_CIRCLE, + }) + expect(href).toBe(`/~${MOCK_CIRCLE.name}/broadcast`) + }) + }) + + describe('page: circleSettings', () => { + it('should return the correct path', () => { + const { href } = toPath({ + page: 'circleSettings', + circle: MOCK_CIRCLE, + }) + expect(href).toBe(`/~${MOCK_CIRCLE.name}/settings`) + }) + }) + + describe('page: circleAnalytics', () => { + it('should return the correct path', () => { + const { href } = toPath({ + page: 'circleAnalytics', + circle: MOCK_CIRCLE, + }) + expect(href).toBe(`/~${MOCK_CIRCLE.name}/analytics`) + }) + }) + + describe('page: circleEditProfile', () => { + it('should return the correct path', () => { + const { href } = toPath({ + page: 'circleEditProfile', + circle: MOCK_CIRCLE, + }) + expect(href).toBe(`/~${MOCK_CIRCLE.name}/settings/edit-profile`) + }) + }) + + describe('page: circleManageInvitation', () => { + it('should return the correct path', () => { + const { href } = toPath({ + page: 'circleManageInvitation', + circle: MOCK_CIRCLE, + }) + expect(href).toBe(`/~${MOCK_CIRCLE.name}/settings/manage-invitation`) + }) + }) + + describe('page: commentDetail', () => { + it('should return the correct path for article comment', () => { + // w/0 parent comment + const { href } = toPath({ + page: 'commentDetail', + comment: { ...MOCK_COMMENT, parentComment: null }, + article: MOCK_ARTILCE, + }) + expect(href).toBe( + `/@${MOCK_ARTILCE.author.userName}/${ + fromGlobalId(MOCK_ARTILCE.id).id + }-${MOCK_ARTILCE.slug}-${MOCK_ARTILCE.mediaHash}#${MOCK_COMMENT.id}` + ) + + // with parent comment + const { href: href2 } = toPath({ + page: 'commentDetail', + comment: MOCK_COMMENT, + article: MOCK_ARTILCE, + }) + expect(href2).toBe( + `/@${MOCK_ARTILCE.author.userName}/${ + fromGlobalId(MOCK_ARTILCE.id).id + }-${MOCK_ARTILCE.slug}-${MOCK_ARTILCE.mediaHash}#${MOCK_COMMENT + .parentComment?.id}-${MOCK_COMMENT.id}` + ) + }) + + it('should return the correct path for circle comment', () => { + // circle discussion + const { href } = toPath({ + page: 'commentDetail', + comment: { + ...MOCK_COMMENT, + type: 'circleDiscussion', + parentComment: null, + }, + circle: MOCK_CIRCLE, + }) + expect(href).toBe(`/~${MOCK_CIRCLE.name}/discussion#${MOCK_COMMENT.id}`) + + // circle broadcast + const { href: href2 } = toPath({ + page: 'commentDetail', + comment: { + ...MOCK_COMMENT, + type: 'circleBroadcast', + parentComment: null, + }, + circle: MOCK_CIRCLE, + }) + expect(href2).toBe(`/~${MOCK_CIRCLE.name}/broadcast#${MOCK_COMMENT.id}`) + }) + }) + + describe('page: draftDetail', () => { + it('should return the correct path', () => { + const { href } = toPath({ page: 'draftDetail', id: MOCK_DRAFT.id }) + expect(href).toBe(`/me/drafts/${MOCK_DRAFT.id}`) + }) + }) + + describe('page: tagDetail', () => { + it('should return the correct path with slug', () => { + const { href } = toPath({ page: 'tagDetail', tag: MOCK_TAG }) + expect(href).toBe( + `/tags/${fromGlobalId(MOCK_TAG.id).id}-${MOCK_TAG.slug}` + ) + }) + + it('should return the correct path w/o slug', () => { + const { href } = toPath({ + page: 'tagDetail', + tag: { ...MOCK_TAG, slug: undefined }, + }) + expect(href).toBe( + `/tags/${fromGlobalId(MOCK_TAG.id).id}-${MOCK_TAG.content}` + ) + }) + + it('should return the correct path w/ feedType', () => { + const { href } = toPath({ + page: 'tagDetail', + tag: MOCK_TAG, + feedType: 'hottest', + }) + expect(href).toBe( + `/tags/${fromGlobalId(MOCK_TAG.id).id}-${MOCK_TAG.slug}?type=hottest` + ) + }) + }) + + describe('page: userProfile', () => { + it('should return the correct path', () => { + const { href } = toPath({ + page: 'userProfile', + userName: MOCK_USER.userName, + }) + expect(href).toBe(`/@${MOCK_USER.userName}`) + }) + }) + + describe('page: userCollections', () => { + it('should return the correct path', () => { + const { href } = toPath({ + page: 'userCollections', + userName: MOCK_USER.userName, + }) + expect(href).toBe(`/@${MOCK_USER.userName}/collections`) + }) + }) + + describe('page: search', () => { + it('should return the correct path with type', () => { + const { href } = toPath({ + page: 'search', + q: 'test', + type: 'article', + }) + expect(href).toBe(`/search?q=test&type=article`) + }) + + it('should return the correct path w/o type', () => { + const { href } = toPath({ + page: 'search', + q: 'test', + }) + expect(href).toBe(`/search?q=test`) + }) + }) + + describe('page: collectionDetail', () => { + it('should return the correct path', () => { + const { href } = toPath({ + page: 'collectionDetail', + userName: MOCK_USER.userName, + collection: MOCK_COLLECTION, + }) + expect(href).toBe( + `/@${MOCK_USER.userName}/collections/${MOCK_COLLECTION.id}` + ) + }) + }) +}) + +describe('utils/route/getSearchType', () => { + it('should return the correct type ', () => { + const articleResult = getSearchType('article') + expect(articleResult).toBe('article') + + const userResult = getSearchType('user') + expect(userResult).toBe('user') + + const tagResult = getSearchType('tag') + expect(tagResult).toBe('tag') + }) + + it('should return undefined when the value is not a valid type', () => { + const result = getSearchType('invalidType') + expect(result).toBeUndefined() + }) +}) diff --git a/src/common/utils/route.ts b/src/common/utils/route.ts index b641df903e..8840bf7b9d 100644 --- a/src/common/utils/route.ts +++ b/src/common/utils/route.ts @@ -41,12 +41,10 @@ interface CommentArgs { } type ToPathArgs = - | ({ + | { page: 'articleDetail' article: ArticleArgs - fragment?: string - // [UtmParam]?: string - } & UtmParams) + } | { page: | 'circleDetail' @@ -57,7 +55,6 @@ type ToPathArgs = | 'circleEditProfile' | 'circleManageInvitation' circle: CircleArgs - fragment?: string } | { page: 'commentDetail' @@ -87,19 +84,18 @@ type ToPathArgs = } /** - * Get `href` and `as` for `` with `args` + * Get `href` for `` with `args` * * (works on SSR & CSR) */ export const toPath = ( - args: ToPathArgs + args: ToPathArgs & UtmParams & { fragment?: string } ): { href: string - pathname?: string - // search?: string - // searchParams?: URLSearchParams | null - // hash?: string } => { + let href = '' + let search = {} + switch (args.page) { case 'articleDetail': { const { @@ -109,137 +105,142 @@ export const toPath = ( author: { userName }, } = args.article - let pathname = `/@${userName}/${slug}-${mediaHash}` + href = `/@${userName}/${slug}-${mediaHash}` + try { - if (id) { - const { id: articleId } = fromGlobalId(id as string) - pathname = `/@${userName}/${articleId}-${slug}${ + const { id: articleId } = fromGlobalId(id as string) + if (id && articleId) { + href = `/@${userName}/${articleId}-${slug}${ mediaHash ? '-' + mediaHash : '' }` } } catch (err) { - console.error(`unable to parse global id:`, { id }, err) + // do nothing } - let search = '' - let searchParams: URLSearchParams | null = null - const { utm_source, utm_medium } = args - if ([utm_source, utm_medium].some(Boolean)) { - searchParams = new URLSearchParams( - [ - ['utm_source', utm_source as string], - ['utm_medium', utm_medium as string], - ].filter(([k, v]) => !!v) - ) - search = '?' + searchParams.toString() - } - - const hash = args.fragment ? `#${args.fragment}` : '' - - return { - href: `${pathname}${search}${hash}`, - pathname, - // search, - // searchParams, - // hash, - } + break } case 'circleDetail': { - return { - href: `/~${args.circle.name}`, - } + href = `/~${args.circle.name}` + break } case 'circleDiscussion': { - const hash = args.fragment ? `#${args.fragment}` : '' - return { - href: `/~${args.circle.name}/discussion${hash}`, - } + href = `/~${args.circle.name}/discussion` + break } case 'circleBroadcast': { - const hash = args.fragment ? `#${args.fragment}` : '' - return { - href: `/~${args.circle.name}/broadcast${hash}`, - } + href = `/~${args.circle.name}/broadcast` + break } case 'circleSettings': { - return { - href: `/~${args.circle.name}/settings`, - } + href = `/~${args.circle.name}/settings` + break } case 'circleAnalytics': { - return { - href: `/~${args.circle.name}/analytics`, - } + href = `/~${args.circle.name}/analytics` + break } case 'circleEditProfile': { - return { - href: `/~${args.circle.name}/settings/edit-profile`, - } + href = `/~${args.circle.name}/settings/edit-profile` + break } case 'circleManageInvitation': { - return { - href: `/~${args.circle.name}/settings/manage-invitation`, - } + href = `/~${args.circle.name}/settings/manage-invitation` + break } case 'commentDetail': { const { parentComment, id, type } = args.comment || {} const fragment = parentComment?.id ? `${parentComment.id}-${id}` : id switch (type) { case 'article': - return toPath({ + href = toPath({ page: 'articleDetail', article: args.article!, fragment, - }) + }).href + break case 'circleDiscussion': case 'circleBroadcast': - return toPath({ + href = toPath({ page: type, // 'circleDiscussion' or 'circleBroadcast' circle: args.circle!, // as { name: string }, fragment, - }) - default: - throw new Error(`unknown comment type: ${type}`) + }).href + break } + break } case 'draftDetail': { - return { - href: `/me/drafts/${args.id}`, - } + href = `/me/drafts/${args.id}` + break } case 'tagDetail': { const { id, slug, content } = args.tag + const name = slug || slugifyTag(content) const { id: numberId } = fromGlobalId(id as string) - const pathname = `/tags/${numberId}-${slug || slugifyTag(content)}` - const typeStr = args.feedType ? `?type=${args.feedType}` : '' - return { - href: `${pathname}${typeStr}`, - pathname, + if (args.feedType) { + search = { + ...search, + type: args.feedType, + } } + href = `/tags/${numberId}-${name}` + break } case 'userProfile': { - return { - href: `/@${args.userName}`, - } + href = `/@${args.userName}` + break } case 'userCollections': { - return { - href: `/@${args.userName}/collections`, - } + href = `/@${args.userName}/collections` + break } case 'collectionDetail': { - return { - href: `/@${args.userName}/collections/${args.collection.id}`, - } + href = `/@${args.userName}/collections/${args.collection.id}` + break } - case 'search': { - const typeStr = args.type ? `&type=${args.type}` : '' - return { - href: `${PATHS.SEARCH}?q=${args.q || ''}${typeStr}`, + search = { + ...search, + q: args.q, + } + + if (args.type) { + search = { + ...search, + type: args.type, + } } + href = PATHS.SEARCH + break } } + + // query string + let searchParams: URLSearchParams = new URLSearchParams( + [ + ['utm_source', args.utm_source as string], + ['utm_medium', args.utm_medium as string], + ['utm_campaign', args.utm_campaign as string], + ['utm_content', args.utm_content as string], + ['utm_id', args.utm_id as string], + ['utm_term', args.utm_term as string], + ...(Object.entries(search) as [string, string][]), + ].filter(([k, v]) => !!v) + ) + if (searchParams.toString()) { + href = `${href}?${searchParams.toString()}` + } + + // hash + if (args.fragment) { + const hash = `#${args.fragment}` + href = `${href}${hash}` + } + + return { + href, + } } export const getTarget = (url?: string) => { diff --git a/src/components/Dialogs/FingerprintDialog/Content.tsx b/src/components/Dialogs/FingerprintDialog/Content.tsx index ce3a553534..a4ddac79c1 100644 --- a/src/components/Dialogs/FingerprintDialog/Content.tsx +++ b/src/components/Dialogs/FingerprintDialog/Content.tsx @@ -3,7 +3,7 @@ import gql from 'graphql-tag' import { useContext, useEffect, useState } from 'react' import { useIntl } from 'react-intl' -import { iscnLinkUrl, translate } from '~/common/utils' +import { translate } from '~/common/utils' import { Button, CopyToClipboard, @@ -53,6 +53,16 @@ const GATEWAYS = gql` } ` +// e.g. https://app.like.co/view/iscn:%2F%2Flikecoin-chain%2FbZs7dmhEk0voCV6vI_eWaHGD-cY32z6zt1scgzV9DVI%2F1 +const isProd = process.env.NEXT_PUBLIC_RUNTIME_ENV === 'production' +const iscnLinkPrefix = isProd + ? 'https://app.like.co/view' + : 'https://app.rinkeby.like.co/view' + +function iscnLinkUrl(iscnId: string) { + return `${iscnLinkPrefix}/${encodeURIComponent(iscnId)}` +} + const FingerprintDialogContent = ({ dataHash, showSecret, diff --git a/src/stories/mocks/index.ts b/src/stories/mocks/index.ts index 05080d2079..366aa7e06a 100644 --- a/src/stories/mocks/index.ts +++ b/src/stories/mocks/index.ts @@ -1,13 +1,13 @@ // User export const MOCK_USER = { __typename: 'User' as any, - id: 'user-0000', + id: 'VXNlcjox', // User:1 userName: 'matty', displayName: 'Matty', avatar: 'https://source.unsplash.com/256x256?user', liker: { __typename: 'Liker' as any, - likerId: 'user-0000', + likerId: 'liker-id-0000', civicLiker: false, }, status: { @@ -72,7 +72,7 @@ export const MOCK_USER = { // Circle export const MOCK_CIRCLE = { __typename: 'Circle' as any, - id: 'circle-0000', + id: 'Q2lyY2xlOjE', // Cirlce:1 state: 'active', name: 'matters_class', displayName: 'Matters 自由課(第一季第二期)', @@ -115,7 +115,7 @@ export const MOCK_CIRCLE = { // Article export const MOCK_ARTILCE = { __typename: 'Article' as any, - id: 'article-0000', + id: 'QXJ0aWNsZTox', // Article:1 title: '中國四川:挑戰世界最危險的公路之一 川藏公路絕美風光', slug: 'slug', mediaHash: 'Qmaisz6NMhDB51cCvNWa1GMS7LU1pAxdF4Ld6Ft9kZEP2a', @@ -168,7 +168,7 @@ export const MOCK_CIRCLE_ARTICLE = { // Comment export const MOCK_PARENT_COMMENT = { __typename: 'Comment' as any, - id: 'comment-0000', + id: 'Q29tbWVudDox', // Comment:1 state: 'active' as any, node: MOCK_ARTILCE, parentComment: null, @@ -179,7 +179,7 @@ export const MOCK_PARENT_COMMENT = { export const MOCK_COMMENT = { __typename: 'Comment' as any, - id: 'comment-0000', + id: 'Q29tbWVudDoy', // Comment:2 state: 'active' as any, node: MOCK_ARTILCE, type: 'article' as any, @@ -195,14 +195,14 @@ export const MOCK_COMMENT = { } export const MOCK_DRAFT = { - id: 'draft-0000', + id: 'RHJhZnQ6MQ', // Draft:1 title: 'draft-title', slug: 'draft-slug', updatedAt: '2020-12-24T07:29:17.682Z', } export const MOCK_COLLECTON = { - id: 'collection-0000', + id: 'Q29sbGVjdGlvbjox', // Collection:1 title: 'collection-title', cover: 'https://source.unsplash.com/256x256?collection', description: 'collection-description', @@ -224,7 +224,7 @@ export const MOCK_CIRCLE_COMMENT = { // Tag export const MOCK_TAG = { __typename: 'Tag' as any, - id: 'tag-0000', + id: 'VGFnOjE', // Tag:1 slug: 'tag-slug', editors: [MOCK_USER], owner: MOCK_USER, diff --git a/src/views/ArticleDetail/index.tsx b/src/views/ArticleDetail/index.tsx index 8f9c5de2bc..32c7180102 100644 --- a/src/views/ArticleDetail/index.tsx +++ b/src/views/ArticleDetail/index.tsx @@ -486,9 +486,7 @@ const ArticleDetail = ({ `https://${process.env.NEXT_PUBLIC_SITE_DOMAIN}${router.asPath}` ) const n = new URL( - `https://${process.env.NEXT_PUBLIC_SITE_DOMAIN}${ - newPath.href || newPath.pathname - }` + `https://${process.env.NEXT_PUBLIC_SITE_DOMAIN}${newPath.href}` ) // hide all utm_ tracking code parameters From d0f08658eb0189f47d8e6efa26e73d8cf8cc4f65 Mon Sep 17 00:00:00 2001 From: robertu <4065233+robertu7@users.noreply.github.com> Date: Sun, 5 Nov 2023 22:28:52 +0800 Subject: [PATCH 12/62] feat(test): mock a date on absolute.test.ts --- src/common/utils/datetime/absolute.test.ts | 17 +++-------------- src/common/utils/route.test.ts | 11 +---------- src/common/utils/route.ts | 3 ++- 3 files changed, 6 insertions(+), 25 deletions(-) diff --git a/src/common/utils/datetime/absolute.test.ts b/src/common/utils/datetime/absolute.test.ts index 0284ac28e4..dc96ccb975 100644 --- a/src/common/utils/datetime/absolute.test.ts +++ b/src/common/utils/datetime/absolute.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import toAbsoluteDate from './absolute' @@ -23,19 +23,8 @@ describe('utils/datetime/absolute', () => { }) it("should format this year's date correctly", () => { - // two days ago - const date = new Date() - date.setDate(date.getDate() - 2) - - const thisYear = new Date().getFullYear() - const thisYearDate = new Date(`${thisYear}-01-01`) - - // skip test - if (thisYearDate > date) { - return - } - - const result = toAbsoluteDate(thisYearDate, 'en') + vi.setSystemTime(new Date(2023, 6, 1)) + const result = toAbsoluteDate(new Date('2023-01-01'), 'en') expect(result).toBe('Jan 1') }) diff --git a/src/common/utils/route.test.ts b/src/common/utils/route.test.ts index f5f6eea674..9dde8a2ab3 100644 --- a/src/common/utils/route.test.ts +++ b/src/common/utils/route.test.ts @@ -11,16 +11,7 @@ import { } from '~/stories/mocks' import { fromGlobalId } from './globalId' -import { - // appendTarget, - // getEncodedCurrent, - getSearchType, - // getTarget, - // redirectToHomePage, - // redirectToLogin, - // redirectToTarget, - toPath, -} from './route' +import { getSearchType, toPath } from './route' describe('utils/route/toPath', () => { describe('page: articleDetail', () => { diff --git a/src/common/utils/route.ts b/src/common/utils/route.ts index 8840bf7b9d..745d66a1a3 100644 --- a/src/common/utils/route.ts +++ b/src/common/utils/route.ts @@ -243,7 +243,7 @@ export const toPath = ( } } -export const getTarget = (url?: string) => { +const getTarget = (url?: string) => { const qs = new URL(url || window.location.href).searchParams const target = encodeURIComponent((qs.get('target') as string) || '') @@ -270,6 +270,7 @@ export const redirectToTarget = ({ : window.location.href let target = decodeURIComponent(getTarget()) + console.log({ target, href: window.location.href }) const isValidTarget = /^((http|https):\/\/)/.test(target) if (!isValidTarget) { target = fallbackTarget From 11969fe6f506c43db714f16b31bb1c7b4d2a71f0 Mon Sep 17 00:00:00 2001 From: robertu <4065233+robertu7@users.noreply.github.com> Date: Sun, 5 Nov 2023 22:53:21 +0800 Subject: [PATCH 13/62] feat(test): add tests for utils/comment --- src/common/utils/comment.test.ts | 95 ++++++++++++++++++++++++++++++++ src/common/utils/comment.ts | 30 +++++++++- src/common/utils/index.ts | 1 - src/common/utils/response.ts | 38 ------------- 4 files changed, 122 insertions(+), 42 deletions(-) create mode 100644 src/common/utils/comment.test.ts delete mode 100644 src/common/utils/response.ts diff --git a/src/common/utils/comment.test.ts b/src/common/utils/comment.test.ts new file mode 100644 index 0000000000..96ab784e7c --- /dev/null +++ b/src/common/utils/comment.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, it } from 'vitest' + +import { ArticleState, CommentState } from '~/gql/graphql' +import { MOCK_ARTILCE, MOCK_COMMENT } from '~/stories/mocks' + +import { filterComments, filterResponses } from './comment' + +describe('utils/comment/filterComments', () => { + it('should not filter comment that are active or collapse', () => { + const comments = [ + { ...MOCK_COMMENT, state: CommentState.Active }, + { ...MOCK_COMMENT, state: CommentState.Collapsed }, + ] + const result = filterComments(comments) + expect(result.length).toEqual(comments.length) + }) + + it('should filter out comment that are banned or archived', () => { + const comments = [ + { ...MOCK_COMMENT, state: CommentState.Banned }, + { ...MOCK_COMMENT, state: CommentState.Archived }, + ] + const result = filterComments(comments) + expect(result.length).toEqual(0) + }) + + it('should filter out comment that are decendant', () => { + const comments = [ + { + ...MOCK_COMMENT, + state: CommentState.Banned, + parentComment: { id: '1' }, + }, + ] + const result = filterComments(comments) + expect(result.length).toEqual(0) + }) + + it('should not filter out comment that are not decendant and has active descendant comments', () => { + const comments = [ + { + ...MOCK_COMMENT, + state: CommentState.Banned, + parentComment: null, + comments: { + edges: [ + { + node: { + ...MOCK_COMMENT, + state: CommentState.Active, + }, + }, + ], + }, + }, + ] + const result = filterComments(comments) + expect(result.length).toEqual(comments.length) + }) + + it('should filter out comment that are not decendant and has banned descendant comments', () => { + const comments = [ + { + ...MOCK_COMMENT, + state: CommentState.Banned, + parentComment: null, + comments: { + edges: [ + { + node: { + ...MOCK_COMMENT, + state: CommentState.Banned, + }, + }, + ], + }, + }, + ] + const result = filterComments(comments) + expect(result.length).toEqual(0) + }) +}) + +describe('utils/comment/filterResponses', () => { + it('should not filter out responses that are articles', () => { + const responses = [ + { ...MOCK_ARTILCE, state: ArticleState.Active }, + { ...MOCK_ARTILCE, state: ArticleState.Banned }, + { ...MOCK_COMMENT, state: CommentState.Active }, + { ...MOCK_COMMENT, state: CommentState.Banned }, + ] + const result = filterResponses(responses) + expect(result.length).toEqual(responses.length - 1) + }) +}) diff --git a/src/common/utils/comment.ts b/src/common/utils/comment.ts index 81448b2916..a3103c2301 100644 --- a/src/common/utils/comment.ts +++ b/src/common/utils/comment.ts @@ -1,4 +1,7 @@ import _get from 'lodash/get' +import _has from 'lodash/has' + +import { CommentState } from '~/gql/graphql' /** * Filter out comment that banned/archived and hasn't descendants @@ -12,9 +15,12 @@ interface Comment { } | null } -export const filterComment = (comment: Comment) => { +const filterComment = (comment: Comment) => { // skip if comment's state is active or collapse - if (comment.state === 'active' || comment.state === 'collapsed') { + if ( + comment.state === CommentState.Active || + comment.state === CommentState.Collapsed + ) { return true } @@ -23,7 +29,8 @@ export const filterComment = (comment: Comment) => { const hasActiveDescendants = descendants.filter( ({ node }: { node: Comment }) => - node.state === 'active' || node.state === 'collapsed' + node.state === CommentState.Active || + node.state === CommentState.Collapsed ).length > 0 // filter out if it's a decendant comment @@ -42,3 +49,20 @@ export const filterComment = (comment: Comment) => { export function filterComments(comments: Comment[]): T[] { return comments.filter(filterComment) as any } + +/** + * Filter out comment that banned/archived and hasn't descendants + * + * @param responses + */ +export function filterResponses(responses: any[]): T[] { + return responses.filter((response) => { + // article + if (_has(response, 'articleState')) { + return true + } + + // comment + return filterComment(response) + }) +} diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index f2fb805177..9fe9be7425 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -15,7 +15,6 @@ export * from './number' export * from './oauth' export * from './pad' export * from './payment' -export * from './response' export * from './route' export * from './storage' export * from './text' diff --git a/src/common/utils/response.ts b/src/common/utils/response.ts deleted file mode 100644 index cc4041f4f3..0000000000 --- a/src/common/utils/response.ts +++ /dev/null @@ -1,38 +0,0 @@ -import _get from 'lodash/get' -import _has from 'lodash/has' - -import { filterComment } from './comment' - -/** - * Filter out comment that banned/archived and hasn't descendants - * - * @param responses - */ -export function filterResponses(responses: any[]): T[] { - return responses.filter((response) => { - // article - if (_has(response, 'articleState')) { - return true - } - - // comment - return filterComment(response) - }) -} - -export const responseStateIs = ( - response: any, - state: 'active' | 'archived' | 'banned' | 'collapsed' -): boolean => { - // comment - if (response.hasOwnProperty('state')) { - return response.state === state - } - - // article - if (response.hasOwnProperty('articleState')) { - return response.articleState === state - } - - return true -} From 75b2fc7dcef9d447424edbfa4638fa5e5ea5be93 Mon Sep 17 00:00:00 2001 From: robertu <4065233+robertu7@users.noreply.github.com> Date: Sun, 5 Nov 2023 23:11:04 +0800 Subject: [PATCH 14/62] feat(test): add tests for utils/payment --- src/common/utils/payment.test.ts | 37 +++++++++++++++++++ src/common/utils/payment.ts | 15 -------- .../Forms/PaymentForm/AddCredit/index.tsx | 4 -- 3 files changed, 37 insertions(+), 19 deletions(-) create mode 100644 src/common/utils/payment.test.ts diff --git a/src/common/utils/payment.test.ts b/src/common/utils/payment.test.ts new file mode 100644 index 0000000000..819a38971e --- /dev/null +++ b/src/common/utils/payment.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest' + +import { calcMattersFee, formatAmount } from './payment' + +describe('utils/payment/calcMattersFee', () => { + it('should calculate fee correctly', () => { + // int + expect(calcMattersFee(0)).toEqual(0) + expect(calcMattersFee(100)).toEqual(20) + expect(calcMattersFee(104)).toEqual(20.8) + + // float + expect(calcMattersFee(0.1)).toEqual(0.02) + expect(calcMattersFee(0.12)).toEqual(0.02) + expect(calcMattersFee(0.123)).toEqual(0.02) + expect(calcMattersFee(100.1)).toEqual(20.02) + expect(calcMattersFee(100.12)).toEqual(20.02) + expect(calcMattersFee(100.123)).toEqual(20.02) + expect(calcMattersFee(100.52)).toEqual(20.1) + }) +}) + +describe('utils/payment/formatAmount', () => { + it('should format amount correctly', () => { + expect(formatAmount(0)).toEqual('0.00') + expect(formatAmount(0.1)).toEqual('0.10') + expect(formatAmount(0.12)).toEqual('0.12') + expect(formatAmount(0.123)).toEqual('0.12') + expect(formatAmount(100)).toEqual('100.00') + expect(formatAmount(100.1)).toEqual('100.10') + expect(formatAmount(100.12)).toEqual('100.12') + expect(formatAmount(100.123)).toEqual('100.12') + expect(formatAmount(100.52)).toEqual('100.52') + expect(formatAmount(100.523)).toEqual('100.52') + expect(formatAmount(100.526)).toEqual('100.53') + }) +}) diff --git a/src/common/utils/payment.ts b/src/common/utils/payment.ts index 931a5f4924..6ebfe6e019 100644 --- a/src/common/utils/payment.ts +++ b/src/common/utils/payment.ts @@ -13,21 +13,6 @@ export const formatAmount = ( .replace(new RegExp(re, 'g'), '$&,') } -/** - * Calculate Stripe Fee by a given amount based on their pricing model: - * - * @see {@url https://stripe.com/en-hk/pricing} - * @see {@url https://support.stripe.com/questions/passing-the-stripe-fee-on-to-customers} - */ -const FEE_FIXED = 2.35 -const FEE_PERCENT = 0.034 - -export const calcStripeFee = (amount: number) => { - const charge = (amount + FEE_FIXED) / (1 - FEE_PERCENT) - const fee = charge - amount - return numRound(fee) -} - const FEE_MATTERS = 0.2 export const calcMattersFee = (amount: number) => { return numRound(amount * FEE_MATTERS) diff --git a/src/components/Forms/PaymentForm/AddCredit/index.tsx b/src/components/Forms/PaymentForm/AddCredit/index.tsx index e329a9240a..edde38b6cb 100644 --- a/src/components/Forms/PaymentForm/AddCredit/index.tsx +++ b/src/components/Forms/PaymentForm/AddCredit/index.tsx @@ -239,10 +239,6 @@ const BaseAddCredit: React.FC = ({ ) - // const fee = calcStripeFee(values.amount) - // const total = numRound(fee + values.amount) - // const total = numRound(values.amount) - if (completed) { return ( <> From d3d39e4f95613ce74e9b8a6920c3ec9c6e53d82f Mon Sep 17 00:00:00 2001 From: robertu <4065233+robertu7@users.noreply.github.com> Date: Mon, 6 Nov 2023 08:48:42 +0800 Subject: [PATCH 15/62] feat(test): use vi.setSystemTime for utils/datetime --- package-lock.json | 34 +++++----------------- package.json | 3 +- src/common/utils/datetime/absolute.test.ts | 15 +++++----- src/common/utils/datetime/relative.test.ts | 32 +++++++++++--------- src/common/utils/datetime/relative.ts | 2 +- src/common/utils/route.ts | 1 - src/common/utils/url.ts | 2 +- 7 files changed, 37 insertions(+), 52 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3e7485477c..fb4467c0e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -90,7 +90,7 @@ "react-window": "^1.8.9", "url-loader": "^4.1.1", "use-debounce": "^9.0.4", - "validator": "^13.9.0", + "validator": "^13.11.0", "viem": "^1.2.12", "wagmi": "^1.3.8" }, @@ -117,7 +117,6 @@ "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^14.5.1", "@types/autosize": "^4.0.1", - "@types/classnames": "^2.3.1", "@types/d3": "^7.0.0", "@types/express": "^4.17.9", "@types/fingerprintjs2": "^2.0.0", @@ -13839,16 +13838,6 @@ "@types/chai": "*" } }, - "node_modules/@types/classnames": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.3.1.tgz", - "integrity": "sha512-zeOWb0JGBoVmlQoznvqXbE0tEC/HONsnoUNH19Hc96NFsTAwTXbTqb8FMYkru1F/iqp7a18Ws3nWJvtA1sHD1A==", - "deprecated": "This is a stub types definition. classnames provides its own type definitions, so you do not need this installed.", - "dev": true, - "dependencies": { - "classnames": "*" - } - }, "node_modules/@types/connect": { "version": "3.4.34", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz", @@ -40751,9 +40740,9 @@ } }, "node_modules/validator": { - "version": "13.9.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.9.0.tgz", - "integrity": "sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA==", + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", "engines": { "node": ">= 0.10" } @@ -52130,15 +52119,6 @@ "@types/chai": "*" } }, - "@types/classnames": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.3.1.tgz", - "integrity": "sha512-zeOWb0JGBoVmlQoznvqXbE0tEC/HONsnoUNH19Hc96NFsTAwTXbTqb8FMYkru1F/iqp7a18Ws3nWJvtA1sHD1A==", - "dev": true, - "requires": { - "classnames": "*" - } - }, "@types/connect": { "version": "3.4.34", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz", @@ -72701,9 +72681,9 @@ } }, "validator": { - "version": "13.9.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.9.0.tgz", - "integrity": "sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA==" + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==" }, "valtio": { "version": "1.10.6", diff --git a/package.json b/package.json index ba4f74bc2e..19e7aba56d 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,7 @@ "react-window": "^1.8.9", "url-loader": "^4.1.1", "use-debounce": "^9.0.4", - "validator": "^13.9.0", + "validator": "^13.11.0", "viem": "^1.2.12", "wagmi": "^1.3.8" }, @@ -147,7 +147,6 @@ "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^14.5.1", "@types/autosize": "^4.0.1", - "@types/classnames": "^2.3.1", "@types/d3": "^7.0.0", "@types/express": "^4.17.9", "@types/fingerprintjs2": "^2.0.0", diff --git a/src/common/utils/datetime/absolute.test.ts b/src/common/utils/datetime/absolute.test.ts index dc96ccb975..964af81524 100644 --- a/src/common/utils/datetime/absolute.test.ts +++ b/src/common/utils/datetime/absolute.test.ts @@ -1,7 +1,11 @@ -import { describe, expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import toAbsoluteDate from './absolute' +beforeEach(() => { + vi.setSystemTime(new Date(2023, 6, 1)) +}) + describe('utils/datetime/absolute', () => { it('should parse a string date', () => { const date = '2022-01-01' @@ -10,20 +14,18 @@ describe('utils/datetime/absolute', () => { }) it("should format today's date correctly", () => { - const date = new Date() + const date = new Date(2023, 6, 1) const result = toAbsoluteDate(date, 'en') expect(result).toContain('Today') }) it("should format yesterday's date correctly", () => { - const date = new Date() - date.setDate(date.getDate() - 1) + const date = new Date(2023, 6, 0) const result = toAbsoluteDate(date, 'en') expect(result).toContain('Yesterday') }) it("should format this year's date correctly", () => { - vi.setSystemTime(new Date(2023, 6, 1)) const result = toAbsoluteDate(new Date('2023-01-01'), 'en') expect(result).toBe('Jan 1') }) @@ -35,8 +37,7 @@ describe('utils/datetime/absolute', () => { }) it('should format a date in the future correctly', () => { - const now = new Date() - const date = new Date(now.getTime() + 1000 * 60 * 60 * 24 * 365) + const date = new Date('2025-01-01') const result = toAbsoluteDate(date, 'en') expect(result).toContain(date.getFullYear()) }) diff --git a/src/common/utils/datetime/relative.test.ts b/src/common/utils/datetime/relative.test.ts index 9892ec03bc..251c84b52b 100644 --- a/src/common/utils/datetime/relative.test.ts +++ b/src/common/utils/datetime/relative.test.ts @@ -1,7 +1,11 @@ -import { describe, expect, it } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import toRelativeDate from './relative' +beforeEach(() => { + vi.setSystemTime(new Date(2023, 6, 1, 8, 30, 0)) +}) + describe('utils/datetime/relative', () => { it('should parse a string date', () => { const date = '2022-01-01' @@ -16,24 +20,26 @@ describe('utils/datetime/relative', () => { }) it('should format a date within this hour but not this minute correctly', () => { - const now = new Date() - const date = new Date(now.getTime() - 1000 * 60) - const result = toRelativeDate(date, 'en') - expect(result).toBe('1 minute ago') + expect(toRelativeDate(new Date(2023, 6, 1, 8, 29), 'en')).toBe( + '1 minute ago' + ) + expect(toRelativeDate(new Date(2023, 6, 1, 8, 28), 'en')).toBe( + '2 minutes ago' + ) }) it('should format a date within today but not this hour correctly', () => { - const now = new Date() - const date = new Date(now.getTime() - 1000 * 60 * 60) - const result = toRelativeDate(date, 'en') - expect(result).toBe('1 hour ago') + expect(toRelativeDate(new Date(2023, 6, 1, 7, 30, 0), 'en')).toBe( + '1 hour ago' + ) + expect(toRelativeDate(new Date(2023, 6, 1, 6, 20, 0), 'en')).toBe( + '2 hours ago' + ) }) it('should format a date within this week but not today correctly', () => { - const now = new Date() - const date = new Date(now.getTime() - 1000 * 60 * 60 * 24) - const result = toRelativeDate(date, 'en') - expect(result).toBe('1 day ago') + expect(toRelativeDate(new Date('2023-06-30'), 'en')).toBe('1 day ago') + expect(toRelativeDate(new Date('2023-06-26'), 'en')).toBe('5 days ago') }) it('should format a date within this year but not this week correctly', () => { diff --git a/src/common/utils/datetime/relative.ts b/src/common/utils/datetime/relative.ts index 44fa0284dc..c8d92116d6 100644 --- a/src/common/utils/datetime/relative.ts +++ b/src/common/utils/datetime/relative.ts @@ -50,7 +50,7 @@ const relative = (date: Date | string | number, lang: Language = 'zh_hant') => { if (isThisHour(date)) { const diffMins = differenceInMinutes(new Date(), date) || 1 - return diffMins + DIFFS[lang][diffMins === 1 ? 'minuteAgo' : 'minuteAgo'] + return diffMins + DIFFS[lang][diffMins === 1 ? 'minuteAgo' : 'minutesAgo'] } if (isToday(date)) { diff --git a/src/common/utils/route.ts b/src/common/utils/route.ts index 745d66a1a3..68a3e5669f 100644 --- a/src/common/utils/route.ts +++ b/src/common/utils/route.ts @@ -270,7 +270,6 @@ export const redirectToTarget = ({ : window.location.href let target = decodeURIComponent(getTarget()) - console.log({ target, href: window.location.href }) const isValidTarget = /^((http|https):\/\/)/.test(target) if (!isValidTarget) { target = fallbackTarget diff --git a/src/common/utils/url.ts b/src/common/utils/url.ts index fc14b5644f..f2ab12f2a2 100644 --- a/src/common/utils/url.ts +++ b/src/common/utils/url.ts @@ -1,4 +1,4 @@ -export { default as isUrl } from 'validator/lib/isUrl' +export { default as isUrl } from 'validator/lib/isURL' import { URL_COLLECTION_DETAIL } from '../enums' From e85ca94d38f93f2529b2ae20b74dcc62a3cd786f Mon Sep 17 00:00:00 2001 From: robertu <4065233+robertu7@users.noreply.github.com> Date: Mon, 6 Nov 2023 10:08:36 +0800 Subject: [PATCH 16/62] feat(test): add tests for utils/article --- src/common/utils/text/article.test.ts | 161 ++++++++++++++++++ src/common/utils/text/article.ts | 11 +- .../CommentFormDialog/CommentForm/index.tsx | 4 +- src/components/Forms/CommentForm/index.tsx | 4 +- 4 files changed, 166 insertions(+), 14 deletions(-) create mode 100644 src/common/utils/text/article.test.ts diff --git a/src/common/utils/text/article.test.ts b/src/common/utils/text/article.test.ts new file mode 100644 index 0000000000..c418fcb5e3 --- /dev/null +++ b/src/common/utils/text/article.test.ts @@ -0,0 +1,161 @@ +import { describe, expect, it } from 'vitest' + +import { + collapseContent, + countChars, + makeSummary, + measureDiffs, + normalizeArticleTitle, + optimizeEmbed, + stripHtml, +} from './article' + +describe('utils/text/article/stripHtml', () => { + it('should remove HTML tags and replace with spaces', () => { + expect(stripHtml('

Hello, world!

')).toBe( + ' Hello, world ! ' + ) + expect(stripHtml('')).toBe('') + }) + + it('should remove HTML tags and replace with custom string', () => { + expect(stripHtml('

Hello, world!

', '-')).toBe( + '-Hello, -world-!-' + ) + expect(stripHtml('

Hello, world!

', '')).toBe( + 'Hello, world!' + ) + }) +}) + +describe('utils/text/article/collapseContent', () => { + it('should collapse content correctly', () => { + expect(collapseContent('

Hello, \nworld!

')).toBe( + 'Hello,world!' + ) + expect(collapseContent('')).toBe('') + }) +}) + +describe('utils/text/article/countChars', () => { + it('should count characters correctly', () => { + // ASCII + expect(countChars('Hello, world!')).toBe(13) + + // Empty + expect(countChars('')).toBe(0) + + // CJK + expect(countChars('你好,世界!')).toBe(12) + + // Emoji + expect(countChars('👋🌍')).toBe(8) + + // Mixed + expect(countChars('Hello, 你好,👋🌍!')).toBe(22) + }) +}) + +describe('utils/text/article/makeSummary', () => { + it('should make summary correctly', () => { + expect( + makeSummary( + '

Hello, world. This is a very long sentence.

' + ) + ).toBe('Hello, world. This is a very long sentence.') + expect(makeSummary('')).toBe('') + }) + + it('should make summary correctly with custom lenth', () => { + expect( + makeSummary( + '

Hello, world. This is a very long sentence.

', + 20, + 0 + ) + ).toBe('Hello, world. This i…') + }) + + it('should make summary correctly with custom buffer', () => { + expect( + makeSummary( + '

Hello, world. This is a very long sentence.

', + 20, + 0 + ) + ).toBe('Hello, world. This i…') + + expect( + makeSummary( + '

Hello, world. This is a very long sentence.

', + 5, + 3 + ) + ).toBe('Hello…') + }) +}) + +describe('utils/text/article/measureDiffs', () => { + it('should measure diffs correctly', () => { + // no diff + expect(measureDiffs('Hello, world!', 'Hello, world!')).toBe(0) + + // suffix + expect(measureDiffs('Hello, world!', 'Hello, world?')).toBe(1) + expect(measureDiffs('Hello, world!', 'Hello, world')).toBe(1) + expect(measureDiffs('Hello, world!', 'Hello, world!!!')).toBe(2) + + // prefix + expect(measureDiffs('Hello, world!', '.Hello, world!')).toBe(1) + expect(measureDiffs('Hello, world!', 'ello, world!')).toBe(1) + expect(measureDiffs('Hello, world!', 'Aello, world!')).toBe(1) + + // middle + expect(measureDiffs('Hello, world!', 'Hello,world!')).toBe(1) + expect(measureDiffs('Hello, world!', 'Hello, sorld!')).toBe(1) + }) +}) + +describe('utils/text/article/normalizeArticleTitle', () => { + it('should normalize article title correctly', () => { + expect(normalizeArticleTitle('Hello, world!', 1)).toBe('…') + expect(normalizeArticleTitle('Hello, world!', 10)).toBe('Hello,…') + expect(normalizeArticleTitle('Hello, world!', 70)).toBe('Hello, world!') + expect( + normalizeArticleTitle( + '你好,世界!你好,世界!你好,世界!你好,世界!你好,世界!你好,世界!你好,世界!', + 70 + ) + ).toBe( + '你好,世界!你好,世界!你好,世界!你好,世界!你好,世界!你好,…' + ) + }) +}) + +describe('utils/text/article/optimizeEmbed', () => { + it('should add loading="lazy" to iframe tags', () => { + const input = '' + const expected = + '' + expect(optimizeEmbed(input)).toBe(expected) + }) + + it('should wrap img tags in a picture tag with a source and img child', () => { + const input = '' + const expected = ` + + + + + + ` + expect(optimizeEmbed(input).trim()).toBe(expected.trim()) + }) +}) diff --git a/src/common/utils/text/article.ts b/src/common/utils/text/article.ts index 79ffa06bec..d152dc7e09 100644 --- a/src/common/utils/text/article.ts +++ b/src/common/utils/text/article.ts @@ -46,15 +46,6 @@ export const makeSummary = (html: string, length = 140, buffer = 20) => { return summary } -/** - * Removes leading and trailing line breaks from (Quill's) HTML string. - */ -export const trimLineBreaks = (html: string) => { - const LINE_BREAK = '


' - const re = new RegExp(`(^(${LINE_BREAK})*)|((${LINE_BREAK})*$)`, 'g') - return html.replace(re, '') -} - /** * Simple words' length counting. */ @@ -102,7 +93,7 @@ export const normalizeArticleTitle = (text: string, limit: number) => { */ export const optimizeEmbed = (content: string) => { return content - .replace(/\