Compare commits

...

18 Commits

Author SHA1 Message Date
Nolan Lawson 7a135f3368
chore: update dependencies (#422) 2024-05-25 09:52:04 -07:00
Nolan Lawson 1cd4b9da68
chore: remove focus-visible polyfill from local dev server (#420) 2024-04-13 09:04:55 -07:00
Nolan Lawson 028f4dc8ed
test: add test to ensure that node is defined (#419) 2024-04-12 19:05:34 -07:00
Nolan Lawson 3d84cf384e 1.21.3 2024-04-09 07:12:08 -07:00
Éric Le Maître e52867681a
fix: improved French translations (#417) 2024-04-09 07:10:03 -07:00
dependabot[bot] 9211adedfa
chore(deps-dev): bump vite from 5.2.4 to 5.2.8 (#416)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.2.4 to 5.2.8.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.2.8/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-05 06:34:10 -07:00
dependabot[bot] 320414bcb7
chore(deps-dev): bump express from 4.19.1 to 4.19.2 (#415)
Bumps [express](https://github.com/expressjs/express) from 4.19.1 to 4.19.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.1...4.19.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-29 17:13:08 -07:00
Nolan Lawson 6a0f4a25ad
chore: remove references to yarn in docs (#414) 2024-03-23 10:52:06 -07:00
Nolan Lawson 6456dbf8a4
chore: switch from yarn to pnpm (#413) 2024-03-23 10:45:43 -07:00
Nolan Lawson c067cbab29 1.21.2 2024-03-21 19:02:47 -07:00
Nolan Lawson 1ae4e30e91
chore: update dependencies (#412) 2024-03-21 19:00:57 -07:00
Nolan Lawson ce950ff740
fix: avoid HTML comments, simplify replacement logic (#409) 2024-03-21 18:43:12 -07:00
Nolan Lawson ff88212004
chore: update node version in CI (#411) 2024-03-17 19:00:14 -07:00
Nolan Lawson 2a57ba18e8
test: centralize fetch mocks in one place (#408) 2024-03-10 20:13:46 -07:00
Nolan Lawson 55872ba996
fix: minor refactor to reduce code size (#406) 2024-03-09 15:40:36 -08:00
Nolan Lawson 15bca2197f
chore: update dependencies (#405) 2024-03-09 13:42:46 -08:00
Nolan Lawson cc1f64d23e
chore: switch from jest to vitest (#404) 2024-03-09 13:21:00 -08:00
Nolan Lawson 7365322a5f
chore: update devDeps (#402) 2024-03-02 10:41:38 -08:00
56 changed files with 9340 additions and 9066 deletions

View File

@ -14,33 +14,20 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/checkout@v4
- run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '22'
cache: 'pnpm'
- run: pnpm i --frozen-lockfile
# via https://github.com/actions/cache/blob/0638051/examples.md#node---yarn
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: yarn install
if: steps.cache.outputs.cache-hit != 'true'
- name: install chromedriver
run: |
yarn --immutable --ignore-scripts
# install the chromedriver corresponding to whatever version of chrome is installed
yarn add --ignore-scripts chromedriver@^$(google-chrome --version | awk '{print $3}' | cut -d. -f1)
PERF=1 yarn build:rollup
yarn benchmark:runtime:setup
pnpm i chromedriver@^$(google-chrome --version | awk '{print $3}' | cut -d. -f1)
PERF=1 pnpm build:rollup
pnpm benchmark:runtime:setup
# first-load
- name: Benchmark first-load
@ -115,4 +102,4 @@ jobs:
report-id: emoji-picker-element-search
path: test/benchmark/search.results.json
pr-bench-name: this-change
base-bench-name: tip-of-tree
base-bench-name: tip-of-tree

View File

@ -8,26 +8,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/checkout@v4
- run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: '20'
# via https://github.com/actions/cache/blob/0638051/examples.md#node---yarn
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
node-version: '22'
cache: 'pnpm'
- run: pnpm i --frozen-lockfile
- uses: preactjs/compressed-size-action@v2
with:
build-script: benchmark:bundle
pattern: "./bundle.js"
compression: "none"
compression: "none"

View File

@ -8,14 +8,16 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v4
- run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'yarn'
node-version: '22'
cache: 'pnpm'
- run: pnpm i --frozen-lockfile
- run: sudo apt-get install fonts-noto-color-emoji
- run: yarn --frozen-lockfile
- run: yarn lint
- run: yarn benchmark:bundlesize
- run: yarn cover
- run: yarn test:leak
- run: pnpm lint
- run: pnpm benchmark:bundlesize
- run: pnpm cover
- run: npx puppeteer browsers install chrome
- run: pnpm test:leak

7
.gitignore vendored
View File

@ -108,13 +108,6 @@ dist
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.pnp.*
/database.js
/database.js.map
/picker.js

View File

@ -1,4 +1 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
./node_modules/.bin/lint-staged

1
.yarnrc.yml Normal file
View File

@ -0,0 +1 @@
nodeLinker: node-modules

View File

@ -1,3 +1,22 @@
## [1.21.3](https://github.com/nolanlawson/emoji-picker-element/compare/v1.21.2...v1.21.3) (2024-04-09)
### Bug Fixes
* improved French translations ([#417](https://github.com/nolanlawson/emoji-picker-element/issues/417)) ([e528676](https://github.com/nolanlawson/emoji-picker-element/commit/e52867681ab07f9eca575e7899f453b9fcd2070a))
## [1.21.2](https://github.com/nolanlawson/emoji-picker-element/compare/v1.21.1...v1.21.2) (2024-03-22)
### Bug Fixes
* avoid HTML comments, simplify replacement logic ([#409](https://github.com/nolanlawson/emoji-picker-element/issues/409)) ([ce950ff](https://github.com/nolanlawson/emoji-picker-element/commit/ce950ff740292e6914ed0744b5587db2f3dcc1f7))
* minor refactor to reduce code size ([#406](https://github.com/nolanlawson/emoji-picker-element/issues/406)) ([55872ba](https://github.com/nolanlawson/emoji-picker-element/commit/55872ba99647425008b5b047960893cca9f88713))
## [1.21.1](https://github.com/nolanlawson/emoji-picker-element/compare/v1.21.0...v1.21.1) (2024-02-17)

View File

@ -2,58 +2,57 @@
## Basic dev workflow
Install
yarn
pnpm i
Run a local dev server on `localhost:3000`:
yarn dev
pnpm dev
## Testing
Lint:
yarn lint
pnpm lint
Fix most lint issues:
yarn lint:fix
pnpm lint:fix
Run the tests:
yarn test
pnpm test
Check code coverage:
yarn cover
pnpm cover
## Other
Benchmark runtime performance:
yarn benchmark:runtime
pnpm benchmark:runtime
Benchmark memory usage:
yarn benchmark:memory
pnpm benchmark:memory
Benchmark bundle size:
yarn benchmark:bundlesize
pnpm benchmark:bundlesize
Benchmark storage size:
yarn benchmark:storage
pnpm benchmark:storage
Run memory leak test:
yarn test:leak
pnpm test:leak
Build the GitHub Pages docs site:
yarn docs
pnpm docs
## FAQs

View File

@ -273,7 +273,7 @@ Here is a full list of options:
### Focus outline
For accessibility reasons, `emoji-picker-element` displays a prominent focus ring. If you want to hide the focus ring for non-keyboard users (e.g. mouse and touch only), then use the [focus-visible](https://github.com/WICG/focus-visible) polyfill, e.g.:
For accessibility reasons, `emoji-picker-element` displays a prominent focus ring for keyboard users. This uses [`:focus-visible`](https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible) under the hood. To properly support [browsers that do not support `:focus-visible`](https://caniuse.com/css-focus-visible), you can use the [focus-visible](https://github.com/WICG/focus-visible) polyfill, e.g.:
```js
import 'focus-visible';

View File

@ -1,8 +0,0 @@
import * as sass from 'sass'
import { minify } from 'csso'
export function buildStyles () {
const file = './src/picker/styles/picker.scss'
const css = sass.compile(file, { style: 'compressed' }).css
return minify(css).css
}

View File

@ -1,22 +0,0 @@
import { buildStyles } from './buildStyles.js'
import { writeFile, mkdirp } from './fs.js'
import path from 'node:path'
const __dirname = path.dirname(new URL(import.meta.url).pathname)
// Build a file containing the CSS just for Jest, because I can't figure out any better way to do this
async function main () {
const styles = buildStyles()
const targetDir = path.join(__dirname, '../node_modules/.cache/emoji-picker-element')
await mkdirp(targetDir)
await writeFile(
path.join(targetDir, 'styles.js'),
`export default ${JSON.stringify(styles)};`,
'utf8'
)
}
main().catch(err => {
console.error(err)
process.exit(1)
})

View File

@ -4,7 +4,7 @@ set -e
cd ./test/benchmark
# Tachometer doesn't seem to be able to locate relative files anywhere but the currect directory. So
# Tachometer doesn't seem to be able to locate relative files anywhere but the current directory. So
# move every file we need right here.
# See also: https://github.com/google/tachometer/issues/244
ln -sf ../../node_modules/emoji-picker-element-data/en/emojibase/data.json ./data.json

View File

@ -38,8 +38,9 @@ for (const benchmark of benchmarks) {
repo: 'https://github.com/nolanlawson/emoji-picker-element.git',
ref: 'master',
setupCommands: [
'yarn --immutable --ignore-scripts',
'PERF=1 yarn build:rollup'
// we're comparing against historical branches, so support yarn as well as pnpm since we switched
'if [ -f yarn.lock ]; then yarn --frozen-lockfile; else pnpm i --frozen-lockfile; fi',
'PERF=1 npm run build:rollup'
]
}
}

View File

@ -0,0 +1,18 @@
import * as sass from 'sass'
import { minify } from 'csso'
export function buildStylesRollupPlugin () {
return {
name: 'build-styles-from-scss',
transform (content, id) {
if (id.includes('picker.scss')) {
const css = sass.compile(id, { style: 'compressed' }).css
const code = `export default ${JSON.stringify(minify(css).css)}`
return {
code,
map: null
}
}
}
}
}

View File

@ -1,52 +0,0 @@
import '@testing-library/jest-dom/jest-globals'
import { jest } from '@jest/globals'
import * as FakeIndexedDB from 'fake-indexeddb'
import { Crypto } from '@peculiar/webcrypto'
import { ResizeObserver } from 'd2l-resize-aware/resize-observer-module.js'
import { deleteDatabase } from '../src/database/databaseLifecycle'
import styles from '../node_modules/.cache/emoji-picker-element/styles.js'
import * as fetchMockJest from 'fetch-mock-jest'
const { IDBFactory, IDBKeyRange } = FakeIndexedDB
// See https://github.com/jsdom/jsdom/issues/3455#issuecomment-1333567714
globalThis.crypto.subtle = new Crypto().subtle
if (!globalThis.performance) {
globalThis.performance = {}
}
if (!globalThis.performance.mark) {
globalThis.performance.mark = () => {}
}
if (!globalThis.performance.measure) {
globalThis.performance.measure = () => {}
}
jest.setTimeout(60000)
globalThis.ResizeObserver = ResizeObserver
process.env.NODE_ENV = 'test'
process.env.STYLES = styles
globalThis.IDBKeyRange = IDBKeyRange
globalThis.indexedDB = new IDBFactory()
// Hack to work around an issue with jest-environment-jsdom https://github.com/jsdom/jsdom/issues/3363
globalThis.structuredClone = globalThis.structuredClone ?? (_ => JSON.parse(JSON.stringify(_)))
beforeAll(() => {
jest.spyOn(globalThis.console, 'log').mockImplementation()
jest.spyOn(globalThis.console, 'warn').mockImplementation()
const fetch = fetchMockJest.default.sandbox()
globalThis.fetch = fetch
globalThis.Response = fetch.Response
})
afterEach(async () => {
// fresh indexedDB for every test
const dbs = await globalThis.indexedDB.databases()
await Promise.all(dbs.map(({ name }) => deleteDatabase(name)))
})

View File

@ -1,9 +0,0 @@
import { minifyHTMLLiterals } from 'minify-html-literals'
export default {
processAsync (source, fileName) {
return minifyHTMLLiterals(source, {
fileName
})
}
}

View File

@ -0,0 +1,14 @@
import { minifyHTMLLiterals } from 'minify-html-literals'
export function minifyHtmlLiteralsRollupPlugin () {
return {
name: 'minify-html-in-tag-template-literals',
transform (content, id) {
if (content.includes('html`')) {
return minifyHTMLLiterals(content, {
fileName: id
})
}
}
}
}

26
config/vitest.setup.js Normal file
View File

@ -0,0 +1,26 @@
import { vi } from 'vitest'
import '@testing-library/jest-dom/vitest'
import { IDBFactory, IDBKeyRange } from 'fake-indexeddb'
import { ResizeObserver } from 'd2l-resize-aware/resize-observer-module.js'
import { deleteDatabase } from '../src/database/databaseLifecycle'
import fetchMock from 'fetch-mock'
beforeAll(() => {
globalThis.ResizeObserver = ResizeObserver
globalThis.IDBKeyRange = IDBKeyRange
globalThis.indexedDB = new IDBFactory()
vi.spyOn(globalThis.console, 'log').mockImplementation(() => undefined)
vi.spyOn(globalThis.console, 'warn').mockImplementation(() => undefined)
})
beforeEach(() => {
globalThis.fetch = fetchMock.sandbox()
globalThis.Response = fetch.Response
})
afterEach(async () => {
// fresh indexedDB for every test
const dbs = await globalThis.indexedDB.databases()
await Promise.all(dbs.map(({ name }) => deleteDatabase(name)))
})

View File

@ -1,30 +0,0 @@
module.exports = {
testEnvironment: 'jsdom',
testMatch: [
'<rootDir>/test/spec/**/*.{spec,test}.{js,jsx,ts,tsx}'
],
transform: {
'^.*PickerTemplate.js$': './config/minifyHtmlInJest.js'
},
moduleFileExtensions: ['js'],
testPathIgnorePatterns: ['node_modules'],
bail: true,
verbose: true,
silent: false,
setupFilesAfterEnv: [
'<rootDir>/config/jest.setup.js'
],
coverageReporters: ['json', 'lcov', 'text', 'html'],
coveragePathIgnorePatterns: [
'bin/',
'test/'
],
coverageThreshold: {
global: {
statements: 100,
branches: 100,
functions: 100,
lines: 100
}
}
}

View File

@ -1,6 +1,6 @@
{
"name": "emoji-picker-element",
"version": "1.21.1",
"version": "1.21.3",
"description": "Lightweight emoji picker distributed as a web component",
"main": "index.js",
"module": "index.js",
@ -18,7 +18,7 @@
"/i18n/*"
],
"scripts": {
"prepare": "run-s build && husky install",
"prepare": "run-s build && husky",
"build": "run-s build:rollup build:i18n build:css-docs build:i18n-docs build:toc",
"build:rollup": "cross-env NODE_ENV=production rollup -c",
"build:css-docs": "node ./bin/generateCssDocs",
@ -44,9 +44,9 @@
"dev:server": "node ./test/adhoc/server.js",
"lint": "standard && stylelint '**/*.scss'",
"lint:fix": "standard --fix && stylelint --fix '**/*.scss'",
"test": "node ./bin/buildStylesForJest.js && NODE_OPTIONS=--experimental-vm-modules jest --runInBand",
"test": "vitest",
"test:adhoc": "node ./test/adhoc/server.js",
"cover": "node ./bin/buildStylesForJest.js && NODE_OPTIONS=--experimental-vm-modules jest --runInBand --coverage",
"cover": "vitest --coverage",
"docs": "node bin/processCustomEmoji.js",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"version": "run-s changelog docs && git add CHANGELOG.md docs"
@ -71,59 +71,59 @@
},
"homepage": "https://github.com/nolanlawson/emoji-picker-element#readme",
"devDependencies": {
"@peculiar/webcrypto": "^1.4.5",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-commonjs": "^25.0.8",
"@rollup/plugin-inject": "^5.0.5",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "^5.0.5",
"@rollup/plugin-strip": "^3.0.4",
"@testing-library/dom": "^9.3.4",
"@testing-library/jest-dom": "^6.4.2",
"@rollup/plugin-terser": "^0.4.4",
"@testing-library/dom": "^10.1.0",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/user-event": "^14.5.2",
"@vitest/coverage-istanbul": "^1.6.0",
"@vitest/ui": "^1.6.0",
"blob-util": "^2.0.2",
"compression": "^1.7.4",
"conventional-changelog-cli": "^2.2.2",
"conventional-changelog-cli": "^5.0.0",
"cross-env": "^7.0.3",
"csso": "^5.0.2",
"csso": "^5.0.5",
"d2l-resize-aware": "github:BrightspaceUI/resize-aware#semver:^1.2.2",
"emoji-picker-element-data": "^1.6.0",
"emojibase-data": "^5.1.1",
"express": "^4.18.2",
"fake-indexeddb": "^5.0.2",
"express": "^4.19.2",
"fake-indexeddb": "^6.0.0",
"fast-glob": "^3.3.2",
"fetch-mock-jest": "^1.5.1",
"fetch-mock": "^9.11.0",
"flat-color-icons": "^1.1.0",
"focus-visible": "^5.2.0",
"get-folder-size": "^4.0.0",
"husky": "^9.0.11",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"lint-staged": "^15.2.2",
"lodash-es": "^4.17.15",
"markdown-table": "^3.0.2",
"jsdom": "^24.0.0",
"lint-staged": "^15.2.5",
"lodash-es": "^4.17.21",
"markdown-table": "^3.0.3",
"markdown-toc": "^1.2.0",
"minify-html-literals": "^1.3.5",
"node-fetch": "^2.7.0",
"npm-run-all": "^4.1.5",
"playwright": "^1.41.2",
"playwright": "^1.44.1",
"pretty-bytes": "^6.1.1",
"puppeteer": "^22.1.0",
"postcss": "^8.4.38",
"puppeteer": "^22.10.0",
"recursive-readdir": "^2.2.3",
"rollup": "^4.12.0",
"rollup": "^4.18.0",
"rollup-plugin-analyzer": "^4.0.0",
"rollup-plugin-terser": "^7.0.2",
"sass": "^1.71.0",
"shx": "^0.3.4",
"sass": "^1.77.2",
"standard": "^17.1.0",
"string.prototype.replaceall": "^1.0.9",
"stylelint": "^16.2.1",
"stylelint": "^16.6.0",
"stylelint-config-recommended-scss": "^14.0.0",
"stylelint-scss": "^6.1.0",
"svgo": "^3.2.0",
"stylelint-scss": "^6.3.0",
"svgo": "^3.3.2",
"tachometer": "^0.7.0",
"terser": "^5.27.1"
"terser": "^5.31.0",
"vitest": "^1.6.0"
},
"//": {
"jsonwebtoken": "comes from tachometer, tachometer is pinned for now due to breaking change, but jsonwebtoken 8 has a vuln"
"jsonwebtoken": "comes from tachometer, jsonwebtoken 8 has a vuln"
},
"resolutions": {
"jsonwebtoken": "^9.0.0"
@ -178,15 +178,13 @@
"emoji-picker"
]
}
]
],
"no-descending-specificity": null
}
},
"lint-staged": {
"*.js": "standard --fix",
"*.(css|scss)": "stylelint --fix '**/*.scss'"
},
"volta": {
"node": "20.9.0",
"yarn": "1.22.19"
}
"packageManager": "pnpm@9.1.2+sha512.127dc83b9ea10c32be65d22a8efb4a65fb952e8fefbdfded39bdc3c97efc32d31b48b00420df2c1187ace28c921c902f0cb5a134a4d032b8b5295cbfa2c681e2"
}

8826
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -3,10 +3,10 @@ import resolve from '@rollup/plugin-node-resolve'
import replace from '@rollup/plugin-replace'
import strip from '@rollup/plugin-strip'
import analyze from 'rollup-plugin-analyzer'
import { buildStyles } from './bin/buildStyles.js'
import { minifyHTMLLiterals } from 'minify-html-literals'
import { minifyHtmlLiteralsRollupPlugin } from './config/minifyHtmlLiteralsRollupPlugin.js'
import { buildStylesRollupPlugin } from './config/buildStylesRollupPlugin.js'
const { NODE_ENV, DEBUG } = process.env
const { NODE_ENV, DEBUG, PERF } = process.env
const dev = NODE_ENV !== 'production'
// Build Database.test.js and Picker.js as separate modules at build times so that they are properly tree-shakeable.
@ -16,9 +16,8 @@ const baseConfig = {
resolve(),
cjs(),
replace({
'process.env.NODE_ENV': dev ? '"development"' : '"production"',
'process.env.PERF': !!process.env.PERF,
'process.env.STYLES': JSON.stringify(buildStyles()),
'import.meta.env.MODE': dev ? '"development"' : '"production"',
'import.meta.env.PERF': !!PERF,
preventAssignment: true
}),
replace({
@ -26,20 +25,12 @@ const baseConfig = {
delimiters: ['', ''],
preventAssignment: true
}),
{
name: 'minify-html-in-tag-template-literals',
transform (content, id) {
if (id.includes('PickerTemplate.js')) {
return minifyHTMLLiterals(content, {
fileName: id
})
}
}
},
minifyHtmlLiteralsRollupPlugin(),
buildStylesRollupPlugin(),
strip({
include: ['**/*.js'],
functions: [
(!dev && !process.env.PERF) && 'performance.*',
(!dev && !PERF) && 'performance.*',
!dev && 'console.log'
].filter(Boolean)
}),

View File

@ -29,7 +29,7 @@ async function doFullDatabaseScanForSingleResult (db, predicate) {
//
// Mini-benchmark for determining the best batch size:
//
// PERF=1 yarn build:rollup && yarn test:adhoc
// PERF=1 pnpm build:rollup && pnpm test:adhoc
//
// (async () => {
// performance.mark('start')

View File

@ -4,7 +4,13 @@ import { binaryStringToArrayBuffer, arrayBufferToBinaryString } from 'blob-util'
export async function jsonChecksum (object) {
performance.mark('jsonChecksum')
const inString = JSON.stringify(object)
const inBuffer = binaryStringToArrayBuffer(inString)
let inBuffer = binaryStringToArrayBuffer(inString)
/* istanbul ignore else */
if (import.meta.env.MODE === 'test') {
// Issue with ArrayBuffer in jsdom https://github.com/vitest-dev/vitest/issues/5365
inBuffer = Buffer.from(new Uint8Array(inBuffer))
}
// this does not need to be cryptographically secure, SHA-1 is fine
const outBuffer = await crypto.subtle.digest('SHA-1', inBuffer)
const outBinString = arrayBufferToBinaryString(outBuffer)

View File

@ -4,6 +4,7 @@ import { DEFAULT_CATEGORY_SORTING, DEFAULT_SKIN_TONE_EMOJI, FONT_FAMILY } from '
import enI18n from './i18n/en.js'
import Database from './ImportedDatabase'
import { queueMicrotask } from './utils/queueMicrotask.js'
import baseStyles from './styles/picker.scss'
const PROPS = [
'customEmoji',
@ -25,7 +26,7 @@ export default class PickerElement extends HTMLElement {
super()
this.attachShadow({ mode: 'open' })
const style = document.createElement('style')
style.textContent = process.env.STYLES + EXTRA_STYLES
style.textContent = baseStyles + EXTRA_STYLES
this.shadowRoot.appendChild(style)
this._ctx = {
// Set defaults

View File

@ -197,7 +197,7 @@ export function createRoot (shadowRoot, props) {
// mount logic
if (!state.emojiVersion) {
detectEmojiSupportLevel().then(level => {
// Can't actually test emoji support in Jest/JSDom, emoji never render in color in Cairo
// Can't actually test emoji support in Jest/Vitest/JSDom, emoji never render in color in Cairo
/* istanbul ignore next */
if (!level) {
state.message = state.i18n.emojiUnsupportedMessage
@ -312,7 +312,7 @@ export function createRoot (shadowRoot, props) {
const { database } = state
const favs = (await Promise.all(MOST_COMMONLY_USED_EMOJI.map(unicode => (
database.getEmojiByUnicodeOrName(unicode)
)))).filter(Boolean) // filter because in Jest tests we don't have all the emoji in the DB
)))).filter(Boolean) // filter because in Jest/Vitest tests we don't have all the emoji in the DB
state.defaultFavoriteEmojis = favs
}
@ -362,7 +362,7 @@ export function createRoot (shadowRoot, props) {
function calculateEmojiGridStyle (node) {
calculateWidth(node, abortSignal, width => {
/* istanbul ignore next */
if (process.env.NODE_ENV !== 'test') { // jsdom throws errors for this kind of fancy stuff
if (import.meta.env.MODE !== 'test') { // jsdom throws errors for this kind of fancy stuff
// read all the style/layout calculations we need to make
const style = getComputedStyle(refs.rootElement)
const newNumColumns = parseInt(style.getPropertyValue('--num-columns'), 10)
@ -467,7 +467,7 @@ export function createRoot (shadowRoot, props) {
createEffect(() => {
// consider initialLoad to be complete when the first tabpanel and favorites are rendered
/* istanbul ignore next */
if (process.env.NODE_ENV !== 'production' || process.env.PERF) {
if (import.meta.env.MODE !== 'production' || import.meta.env.PERF) {
if (state.currentEmojis.length && state.currentFavorites.length && state.initialLoad) {
state.initialLoad = false
requestPostAnimationFrame(() => performance.measure('initialLoad', 'initialLoad'))

View File

@ -2,11 +2,12 @@ import { getFromMap, parseTemplate, toString } from './utils.js'
const parseCache = new WeakMap()
const domInstancesCache = new WeakMap()
// This needs to be a symbol because it needs to be different from any possible output of a key function
const unkeyedSymbol = Symbol('un-keyed')
// for debugging
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
if (import.meta.env.MODE !== 'production') {
window.parseCache = parseCache
window.domInstancesCache = domInstancesCache
}
@ -37,7 +38,7 @@ function doChildrenNeedRerender (parentNode, newChildren) {
oldChildrenCount++
}
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && oldChildrenCount !== parentNode.children.length) {
if (import.meta.env.MODE !== 'production' && oldChildrenCount !== parentNode.children.length) {
throw new Error('parentNode.children.length is different from oldChildrenCount, it should not be')
}
// if new children length is different from old, we must re-render
@ -54,7 +55,7 @@ function patchChildren (newChildren, instanceBinding) {
needsRerender = doChildrenNeedRerender(targetParentNode, newChildren)
} else { // first render of list
needsRerender = true
instanceBinding.targetNode = undefined // placeholder comment not needed anymore, free memory
instanceBinding.targetNode = undefined // placeholder node not needed anymore, free memory
instanceBinding.targetParentNode = targetParentNode = targetNode.parentNode
}
// avoid re-rendering list if the dom nodes are exactly the same before and after
@ -94,20 +95,16 @@ function patch (expressions, instanceBindings) {
} else if (expression instanceof Element) { // html tag template returning a DOM element
newNode = expression
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && newNode === targetNode) {
if (import.meta.env.MODE !== 'production' && newNode === targetNode) {
// it seems impossible for the framework to get into this state, may as well assert on it
// worst case scenario is we lose focus if we call replaceWith on the same node
throw new Error('the newNode and targetNode are the same, this should never happen')
}
targetNode.replaceWith(newNode)
} else { // primitive - string, number, etc
if (targetNode.nodeType === Node.TEXT_NODE) { // already transformed into a text node
// nodeValue is faster than textContent supposedly https://www.youtube.com/watch?v=LY6y3HbDVmg
targetNode.nodeValue = toString(expression)
} else { // replace comment or whatever was there before with a text node
newNode = document.createTextNode(toString(expression))
targetNode.replaceWith(newNode)
}
// nodeValue is faster than textContent supposedly https://www.youtube.com/watch?v=LY6y3HbDVmg
// note we may be replacing the value in a placeholder text node
targetNode.nodeValue = toString(expression)
}
if (newNode) {
instanceBinding.targetNode = newNode
@ -140,7 +137,7 @@ function parse (tokens) {
case '<': {
const nextChar = token.charAt(j + 1)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !/[/a-z]/.test(nextChar)) {
if (import.meta.env.MODE !== 'production' && !/[/a-z]/.test(nextChar)) {
// we don't need to support comments ('<!') because we always use html-minify-literals
// also we don't support '<' inside tags, e.g. '<div> 2 < 3 </div>'
throw new Error('framework currently only supports a < followed by / or a-z')
@ -161,7 +158,7 @@ function parse (tokens) {
}
case '=': {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !withinTag) {
if (import.meta.env.MODE !== 'production' && !withinTag) {
// we don't currently support '=' anywhere but inside a tag, e.g.
// we don't support '<div>2 + 2 = 4</div>'
throw new Error('framework currently does not support = anywhere but inside a tag')
@ -194,15 +191,17 @@ function parse (tokens) {
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
if (import.meta.env.MODE !== 'production') {
// remind myself that this object is supposed to be immutable
Object.freeze(binding)
}
bindings.push(binding)
// add a placeholder comment that we can find later
htmlString += (!withinTag && !withinAttribute) ? `<!--${bindings.length - 1}-->` : ''
if (!withinTag && !withinAttribute) {
// Add a placeholder text node, so we can find it later. Note we only support one dynamic child text node
htmlString += ' '
}
}
const template = parseTemplate(htmlString)
@ -213,21 +212,6 @@ function parse (tokens) {
}
}
function findPlaceholderComment (element, bindingId) {
// If we had a lot of placeholder comments to find, it would make more sense to build up a map once
// rather than search the DOM every time. But it turns out that we always only have one child,
// and it's the comment node, so searching every time is actually faster.
let childNode = element.firstChild
while (childNode) {
// Note that minify-html-literals has already removed all non-framework comments
// So we just need to look for comments that have exactly the bindingId as its text content
if (childNode.nodeType === Node.COMMENT_NODE && childNode.nodeValue === toString(bindingId)) {
return childNode
}
childNode = childNode.nextSibling
}
}
function traverseAndSetupBindings (dom, elementsToBindings) {
const instanceBindings = []
// traverse dom
@ -243,11 +227,23 @@ function traverseAndSetupBindings (dom, elementsToBindings) {
const targetNode = binding.attributeName
? element // attribute binding, just use the element itself
: findPlaceholderComment(element, i) // not an attribute binding, so has a placeholder comment
: element.firstChild // not an attribute binding, so has a placeholder text node
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !targetNode) {
throw new Error('targetNode should not be undefined')
if (import.meta.env.MODE !== 'production') {
// We only support exactly one placeholder text node inside an element, which simplifies
// the implementation a lot. Also, minify-html-literals should handle any whitespace
// around the expression, so we should only ever see e.g. `<div>${expr}</div>`
if (
!binding.attributeName &&
!(element.childNodes.length === 1 && element.childNodes[0].nodeType === Node.TEXT_NODE)
) {
throw new Error('framework only supports exactly one dynamic child text node')
}
if (!targetNode) {
throw new Error('targetNode should not be undefined')
}
}
const instanceBinding = {
@ -258,7 +254,7 @@ function traverseAndSetupBindings (dom, elementsToBindings) {
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
if (import.meta.env.MODE !== 'production') {
// remind myself that this object is supposed to be monomorphic (for better JS engine perf)
Object.seal(instanceBinding)
}

View File

@ -17,7 +17,7 @@ export function createState (abortSignal) {
return
}
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && recursionDepth === MAX_RECURSION_DEPTH) {
if (import.meta.env.MODE !== 'production' && recursionDepth === MAX_RECURSION_DEPTH) {
throw new Error('max recursion depth, you probably didn\'t mean to do this')
}
const observersToRun = [...dirtyObservers]
@ -38,7 +38,6 @@ export function createState (abortSignal) {
const state = new Proxy({}, {
get (target, prop) {
// console.log('reactivity: get', prop)
if (currentObserver) {
let observers = propsToObservers.get(prop)
if (!observers) {
@ -50,7 +49,6 @@ export function createState (abortSignal) {
return target[prop]
},
set (target, prop, newValue) {
// console.log('reactivity: set', prop, newValue)
target[prop] = newValue
const observers = propsToObservers.get(prop)
if (observers) {
@ -82,7 +80,7 @@ export function createState (abortSignal) {
// for debugging
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
if (import.meta.env.MODE !== 'production') {
window.state = state
}
@ -91,7 +89,7 @@ export function createState (abortSignal) {
destroyed = true
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
if (import.meta.env.MODE !== 'production') {
delete window.state
}
})

View File

@ -16,7 +16,7 @@ export function parseTemplate (htmlString) {
template.innerHTML = htmlString
/* istanbul ignore next */
if (process.env.NODE_ENV !== 'production') {
if (import.meta.env.MODE !== 'production') {
if (template.content.children.length !== 1) {
throw new Error('only 1 child allowed for now')
}

View File

@ -1,18 +1,18 @@
export default {
categoriesLabel: 'Catégories',
emojiUnsupportedMessage: 'Votre navigateur ne soutient pas les emojis en couleur.',
emojiUnsupportedMessage: 'Votre navigateur ne supporte pas les emojis en couleur.',
favoritesLabel: 'Favoris',
loadingMessage: 'Chargement en cours…',
networkErrorMessage: 'Impossible de charger les emojis.',
regionLabel: 'Choisir un emoji',
searchDescription: 'Quand les résultats sont disponisbles, appuyez la fleche vers le haut ou le bas et la touche entrée pour choisir.',
searchDescription: 'Lorsque les résultats sont affichés, utilisez les flèches haut/bas pour naviguer et la touche entrée pour sélectionner.',
searchLabel: 'Rechercher',
searchResultsLabel: 'Résultats',
skinToneDescription: 'Quand disponible, appuyez la fleche vers le haut ou le bas et la touch entrée pour choisir.',
skinToneDescription: 'Quand disponible, utilisez les flèches haut/bas pour naviguer et la touche entrée pour sélectionner.',
skinToneLabel: 'Choisir une couleur de peau (actuellement {skinTone})',
skinTonesLabel: 'Couleurs de peau',
skinTones: [
'Défaut',
'Par défaut',
'Clair',
'Moyennement clair',
'Moyen',
@ -20,15 +20,15 @@ export default {
'Sombre'
],
categories: {
custom: 'Customisé',
'smileys-emotion': 'Les smileyes et les émoticônes',
'people-body': 'Les gens et le corps',
'animals-nature': 'Les animaux et la nature',
'food-drink': 'La nourriture et les boissons',
'travel-places': 'Les voyages et les endroits',
activities: 'Les activités',
objects: 'Les objets',
symbols: 'Les symbols',
flags: 'Les drapeaux'
custom: 'Personnalisé',
'smileys-emotion': 'Émoticônes',
'people-body': 'Corps et métiers',
'animals-nature': 'Animaux et nature',
'food-drink': 'Nourriture et boissons',
'travel-places': 'Voyages et lieux',
activities: 'Activités',
objects: 'Objets',
symbols: 'Symboles',
flags: 'Drapeaux'
}
}

View File

@ -1,7 +1,13 @@
// get the width of the text inside of a DOM node, via https://stackoverflow.com/a/59525891/680742
export function calculateTextWidth (node) {
// skip running this in jest/vitest because we don't need to check for emoji support in that environment
/* istanbul ignore else */
if (process.env.NODE_ENV === 'test') {
if (import.meta.env.MODE === 'test') {
// sanity check to make sure the node is defined properly
/* istanbul ignore if */
if (!node) {
throw new Error('node should not be undefined/null')
}
return 1
} else {
const range = document.createRange()

View File

@ -15,7 +15,7 @@ export const detectEmojiSupportLevel = () => {
))
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
if (import.meta.env.MODE !== 'production') {
promise.then(emojiSupportLevel => {
console.log('emoji support level', emojiSupportLevel)
})

View File

@ -8,7 +8,7 @@
import { FONT_FAMILY } from '../constants'
import { versionsAndTestEmoji } from '../../../bin/versionsAndTestEmoji'
// only used in jest tests
// only used in jest/vitest tests
let simulateCanvasError = false
export function setSimulateCanvasError (value) {
simulateCanvasError = value
@ -42,7 +42,7 @@ const compareFeatures = (feature1, feature2) => {
}
export function testColorEmojiSupported (text) {
if (process.env.NODE_ENV === 'test') {
if (import.meta.env.MODE === 'test') {
if (simulateCanvasError) {
throw new Error('canvas error')
} else if (simulateOldBrowser) {
@ -51,7 +51,7 @@ export function testColorEmojiSupported (text) {
.map(([emoji]) => emoji)
.includes(text)
}
return true // avoid using canvas in jest
return true // avoid using canvas in jest/vitest
}
// Render white and black and then compare them to each other and ensure they're the same
// color, and neither one is black. This shows that the emoji was rendered in color.

View File

@ -6,7 +6,7 @@ import { requestAnimationFrame } from './requestAnimationFrame'
export let resizeObserverSupported = typeof ResizeObserver === 'function'
// only used in jest tests
// only used in jest/vitest tests
export const resetResizeObserverSupported = () => {
resizeObserverSupported = typeof ResizeObserver === 'function'
}

View File

@ -47,7 +47,6 @@
</head>
<body>
<button class="delete">Delete database</button>
<script src="/node_modules/focus-visible/dist/focus-visible.js"></script>
<script type="module">
import { Picker, Database } from '/index.js'
(async () => {
@ -89,7 +88,6 @@
}
const picker = new Picker(opts)
picker.addEventListener('emoji-click', e => console.log(e))
applyFocusVisiblePolyfill(picker.shadowRoot)
document.body.appendChild(picker)
})()
</script>

View File

@ -1,4 +1,4 @@
import { terser } from 'rollup-plugin-terser'
import terser from '@rollup/plugin-terser'
export default {
input: './index.js',

View File

@ -4,6 +4,7 @@ import {
ALL_EMOJI_NO_ETAG, tick, mockFrenchDataSource, FR_EMOJI, truncatedEmoji, NO_SHORTCODES, mockDataSourceWithNoShortcodes
} from '../shared'
import trimEmojiData from '../../../src/trimEmojiData'
import { mockFetch, mockGetAndHead } from '../mockFetch.js'
describe('database tests', () => {
beforeEach(basicBeforeEach)
@ -19,57 +20,67 @@ describe('database tests', () => {
test('calls GET first and HEAD afterwards', async () => {
let db = new Database({ dataSource: ALL_EMOJI })
await db.ready()
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(ALL_EMOJI, undefined)
expect(fetch.calls().length).toBe(1)
expect(fetch.lastUrl()).toBe(ALL_EMOJI)
expect(fetch.lastOptions()).toBe(undefined)
await db.close()
db = new Database({ dataSource: ALL_EMOJI })
await db.ready()
await tick(5) // the HEAD request is done asynchronously, so wait for it
expect(fetch).toHaveBeenCalledTimes(2)
expect(fetch).toHaveBeenLastCalledWith(ALL_EMOJI, { method: 'HEAD' })
expect(fetch.calls().length).toBe(2)
expect(fetch.lastUrl()).toBe(ALL_EMOJI)
expect(fetch.lastOptions()).toEqual({ method: 'HEAD' })
await db.delete()
})
test('calls GET first and tries HEAD if ETag unavailable', async () => {
let db = new Database({ dataSource: ALL_EMOJI_NO_ETAG })
await db.ready()
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(ALL_EMOJI_NO_ETAG, undefined)
expect(fetch.calls().length).toBe(1)
expect(fetch.lastUrl()).toBe(ALL_EMOJI_NO_ETAG)
expect(fetch.lastOptions()).toBe(undefined)
await db.close()
db = new Database({ dataSource: ALL_EMOJI_NO_ETAG })
await db.ready()
await tick(5) // the request is done asynchronously, so wait for it
expect(fetch).toHaveBeenCalledTimes(3)
expect(fetch).toHaveBeenNthCalledWith(2, ALL_EMOJI_NO_ETAG, { method: 'HEAD' })
expect(fetch).toHaveBeenLastCalledWith(ALL_EMOJI_NO_ETAG, undefined)
expect(fetch.calls().length).toBe(3)
expect(fetch.calls().at(-2)[0]).toBe(ALL_EMOJI_NO_ETAG)
expect(fetch.calls().at(-2)[1]).toEqual({ method: 'HEAD' })
expect(fetch.lastUrl()).toBe(ALL_EMOJI_NO_ETAG)
expect(fetch.lastOptions()).toBe(undefined)
await db.delete()
})
test('database deletion actually deletes and causes re-fetch', async () => {
let db = new Database({ dataSource: ALL_EMOJI })
await db.ready()
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(ALL_EMOJI, undefined)
expect(fetch.calls().length).toBe(1)
expect(fetch.lastUrl()).toBe(ALL_EMOJI)
expect(fetch.lastOptions()).toBe(undefined)
await db.delete()
db = new Database({ dataSource: ALL_EMOJI })
await db.ready()
expect(fetch).toHaveBeenCalledTimes(2)
expect(fetch).toHaveBeenLastCalledWith(ALL_EMOJI, undefined)
expect(fetch.calls().length).toBe(2)
expect(fetch.lastUrl()).toBe(ALL_EMOJI)
expect(fetch.lastOptions()).toBe(undefined)
await db.delete()
})
test('misconfigured server where ETag in GET but not HEAD still works', async () => {
let db = new Database({ dataSource: ALL_EMOJI_MISCONFIGURED_ETAG })
await db.ready()
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(ALL_EMOJI_MISCONFIGURED_ETAG, undefined)
expect(fetch.calls().length).toBe(1)
expect(fetch.lastUrl()).toBe(ALL_EMOJI_MISCONFIGURED_ETAG)
expect(fetch.lastOptions()).toBe(undefined)
await db.close()
db = new Database({ dataSource: ALL_EMOJI_MISCONFIGURED_ETAG })
await db.ready()
await tick(5) // the request is done asynchronously, so wait for it
expect(fetch).toHaveBeenCalledTimes(3)
expect(fetch).toHaveBeenNthCalledWith(2, ALL_EMOJI_MISCONFIGURED_ETAG, { method: 'HEAD' })
expect(fetch).toHaveBeenLastCalledWith(ALL_EMOJI_MISCONFIGURED_ETAG, undefined)
expect(fetch.calls().length).toBe(3)
expect(fetch.calls().at(-2)[0]).toBe(ALL_EMOJI_MISCONFIGURED_ETAG)
expect(fetch.calls().at(-2)[1]).toEqual({ method: 'HEAD' })
expect(fetch.lastUrl()).toBe(ALL_EMOJI_MISCONFIGURED_ETAG)
expect(fetch.lastOptions()).toBe(undefined)
await db.delete()
})
@ -79,11 +90,11 @@ describe('database tests', () => {
const EMPTY = 'empty.json'
const NULL_ARRAY = 'null-array.json'
const BAD_OBJECT = 'bad-object.json'
fetch.get(NULL, () => new Response('null'))
fetch.get(NOT_ARRAY, () => new Response('{}'))
fetch.get(EMPTY, () => new Response('[]'))
fetch.get(NULL_ARRAY, () => new Response('[null]'))
fetch.get(BAD_OBJECT, () => new Response('[{"missing": true}]'))
mockFetch('get', NULL, 'null')
mockFetch('get', NOT_ARRAY, '{}')
mockFetch('get', EMPTY, '[]')
mockFetch('get', NULL_ARRAY, '[null]')
mockFetch('get', BAD_OBJECT, '[{"missing": true}]')
const makeDB = async (dataSource) => {
const db = new Database({ dataSource })
@ -142,6 +153,7 @@ describe('database tests', () => {
await db1.ready()
const db2 = new Database({ dataSource: ALL_EMOJI })
await db2.ready()
await db2._lazyUpdate // TODO [#407] Skipping this causes an InvalidStateError in IDB
await db1.close()
expect((await db1.getEmojiByUnicodeOrName('🐵')).annotation).toBe('monkey face')
await db2.close()
@ -185,8 +197,7 @@ describe('database tests', () => {
test('basic trimEmojiData test', async () => {
const trimmed = trimEmojiData(truncatedEmoji)
const dataSource = 'trimmed.js'
fetch.get(dataSource, () => new Response(JSON.stringify(trimmed), { headers: { ETag: 'W/trim' } }))
fetch.head(dataSource, () => new Response(null, { headers: { ETag: 'W/trim' } }))
mockGetAndHead(dataSource, trimmed, { headers: { ETag: 'W/trim' } })
const db = new Database({ dataSource })
const emojis = await db.getEmojiBySearchQuery('face')

View File

@ -4,29 +4,32 @@ describe('basic fetch tests', () => {
beforeEach(basicBeforeEach)
afterEach(basicAfterEach)
test('make sure fetch-mock-jest is working correctly', async () => {
expect(fetch).toHaveBeenCalledTimes(0)
test('make sure fetch-mock is working correctly', async () => {
expect(fetch.calls().length).toBe(0)
const resp = await fetch(ALL_EMOJI)
expect(resp.headers.get('etag')).toBe('W/xxx')
expect(await (resp).json()).toEqual(truncatedEmoji)
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(ALL_EMOJI, undefined)
expect(fetch.calls().length).toBe(1)
expect(fetch.lastUrl()).toBe(ALL_EMOJI)
expect(fetch.lastOptions()).toBe(undefined)
})
test('make sure fetch-mock-jest is working correctly 2', async () => {
expect(fetch).toHaveBeenCalledTimes(0)
test('make sure fetch-mock is working correctly 2', async () => {
expect(fetch.calls().length).toBe(0)
const resp = await fetch(ALL_EMOJI_NO_ETAG)
expect(resp.headers.get('etag')).toBeFalsy()
expect(await (resp).json()).toEqual(truncatedEmoji)
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(ALL_EMOJI_NO_ETAG, undefined)
expect(fetch.calls().length).toBe(1)
expect(fetch.lastUrl()).toBe(ALL_EMOJI_NO_ETAG)
expect(fetch.lastOptions()).toBe(undefined)
})
test('make sure fetch-mock-jest is working correctly 3', async () => {
expect(fetch).toHaveBeenCalledTimes(0)
test('make sure fetch-mock is working correctly 3', async () => {
expect(fetch.calls().length).toBe(0)
const resp = await fetch(ALL_EMOJI, { method: 'HEAD' })
expect(resp.headers.get('etag')).toBe('W/xxx')
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(ALL_EMOJI, { method: 'HEAD' })
expect(fetch.calls().length).toBe(1)
expect(fetch.lastUrl()).toBe(ALL_EMOJI)
expect(fetch.lastOptions()).toEqual({ method: 'HEAD' })
})
})

View File

@ -2,6 +2,7 @@ import allEmoji from 'emoji-picker-element-data/en/emojibase/data.json'
import Database from '../../../src/database/Database'
import { pick, omit } from 'lodash-es'
import { basicAfterEach, basicBeforeEach, ALL_EMOJI, truncatedEmoji } from '../shared'
import { mockGetAndHead } from '../mockFetch.js'
// order can change from version to version
const expectToBeSorted = results => {
@ -128,10 +129,7 @@ describe('getEmojiBySearchQuery', () => {
const EMOJI_WITH_APOS = 'http://localhost/apos.json'
fetch.get(EMOJI_WITH_APOS, () => new Response(JSON.stringify(emojiWithTwelveOclock), {
headers: { ETag: 'W/apos' }
}))
fetch.head(EMOJI_WITH_APOS, () => new Response(null, { headers: { ETag: 'W/apos' } }))
mockGetAndHead(EMOJI_WITH_APOS, emojiWithTwelveOclock, { headers: { ETag: 'W/apos' } })
const db = new Database({ dataSource: EMOJI_WITH_APOS })
@ -159,10 +157,7 @@ describe('getEmojiBySearchQuery', () => {
const EMOJI = 'http://localhost/apos.json'
fetch.get(EMOJI, () => new Response(JSON.stringify(emoji), {
headers: { ETag: 'W/blond' }
}))
fetch.head(EMOJI, () => new Response(null, { headers: { ETag: 'W/blond' } }))
mockGetAndHead(EMOJI, emoji, { headers: { ETag: 'W/blond' } })
const db = new Database({ dataSource: EMOJI })

View File

@ -1,5 +1,6 @@
import { ALL_EMOJI, basicAfterEach, basicBeforeEach, truncatedEmoji } from '../shared'
import Database from '../../../src/database/Database'
import { mockGetAndHead } from '../mockFetch.js'
describe('getEmojiByShortcode', () => {
beforeEach(basicBeforeEach)
@ -63,9 +64,7 @@ describe('getEmojiByShortcode', () => {
}
}
fetch
.get(dataSource, () => new Response(JSON.stringify(emojis), { headers: { ETag: 'W/optional' } }))
.head(dataSource, () => new Response(null, { headers: { ETag: 'W/optional' } }))
mockGetAndHead(dataSource, emojis, { headers: { ETag: 'W/optional' } })
const db = new Database({ dataSource })

View File

@ -1,6 +1,7 @@
import allEmoji from 'emoji-picker-element-data/en/emojibase/data.json'
import { ALL_EMOJI, basicAfterEach, basicBeforeEach, truncatedEmoji } from '../shared'
import Database from '../../../src/database/Database'
import { mockGetAndHead } from '../mockFetch.js'
describe('getEmojiByUnicode', () => {
beforeEach(basicBeforeEach)
@ -14,10 +15,7 @@ describe('getEmojiByUnicode', () => {
]
const EMOJI_WITH_PIRATES = 'http://localhost/pirate.json'
fetch.get(EMOJI_WITH_PIRATES, () => new Response(JSON.stringify(emojiPlusPirateFlag), {
headers: { ETag: 'W/yarrr' }
}))
fetch.head(EMOJI_WITH_PIRATES, () => new Response(null, { headers: { ETag: 'W/yarrr' } }))
mockGetAndHead(EMOJI_WITH_PIRATES, emojiPlusPirateFlag, { headers: { ETag: 'W/yarrr' } })
const db = new Database({ dataSource: EMOJI_WITH_PIRATES })

View File

@ -1,22 +1,21 @@
import { jest } from '@jest/globals'
import { vi } from 'vitest'
import { ALL_EMOJI, basicAfterEach, basicBeforeEach } from '../shared'
import Database from '../../../src/database/Database'
import { mock500GetAndHead } from '../mockFetch.js'
describe('offline first', () => {
beforeEach(() => {
basicBeforeEach()
jest.spyOn(console, 'warn').mockImplementation(() => {})
vi.spyOn(console, 'warn').mockImplementation(() => undefined)
})
afterEach(basicAfterEach)
test('basic offline first test', async () => {
let db = new Database({ dataSource: ALL_EMOJI })
await db.close()
fetch.mockClear()
fetch.reset()
fetch.get(ALL_EMOJI, { body: null, status: 500 })
fetch.head(ALL_EMOJI, { body: null, status: 500 })
mock500GetAndHead(ALL_EMOJI)
db = new Database({ dataSource: ALL_EMOJI })
await db.ready()
@ -28,8 +27,7 @@ describe('offline first', () => {
test('basic error test', async () => {
const ERROR = 'error.json'
fetch.get(ERROR, { body: null, status: 500 })
fetch.head(ERROR, { body: null, status: 500 })
mock500GetAndHead(ERROR)
const db = new Database({ dataSource: ERROR })
await (expect(() => db.ready())).rejects.toThrow()

View File

@ -1,12 +1,11 @@
import { ALL_EMOJI, ALL_EMOJI_NO_ETAG, basicAfterEach, basicBeforeEach, tick, truncatedEmoji } from '../shared'
import Database from '../../../src/database/Database'
import allEmoji from 'emoji-picker-element-data/en/emojibase/data.json'
import { mockGetAndHead } from '../mockFetch.js'
function mockEmoji (dataSource, data, etag) {
fetch.mockClear()
fetch.reset()
fetch.get(dataSource, () => new Response(JSON.stringify(data), etag && { headers: { ETag: etag } }))
fetch.head(dataSource, () => new Response(null, etag && { headers: { ETag: etag } }))
mockGetAndHead(dataSource, data, etag && { headers: { ETag: etag } })
}
async function testDataChange (firstData, secondData, firstCallback, secondCallback, thirdCallback) {
@ -51,21 +50,25 @@ describe('database second load and update', () => {
await testDataChange(truncatedEmoji, changedEmoji, async (db, dataSource) => {
// first load
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(dataSource, undefined)
expect(fetch.calls().length).toBe(1)
expect(fetch.lastUrl()).toBe(dataSource)
expect(fetch.lastOptions()).toBe(undefined)
expect((await db.getEmojiByShortcode('rofl')).annotation).toBe('rolling on the floor laughing')
expect(await db.getEmojiByShortcode('weary_cat')).toBeFalsy()
}, async (db, dataSource) => {
// second load
expect(fetch).toHaveBeenCalledTimes(2)
expect(fetch).toHaveBeenLastCalledWith(dataSource, undefined)
expect(fetch).toHaveBeenNthCalledWith(1, dataSource, { method: 'HEAD' })
expect(fetch.calls().length).toBe(2)
expect(fetch.lastUrl()).toBe(dataSource)
expect(fetch.lastOptions()).toBe(undefined)
expect(fetch.calls().at(-2)[0]).toBe(dataSource)
expect(fetch.calls().at(-2)[1]).toEqual({ method: 'HEAD' })
expect((await db.getEmojiByShortcode('rofl'))).toBeFalsy()
expect((await db.getEmojiByShortcode('pineapple')).annotation).toBe('pineapple')
}, async (db, dataSource) => {
// third load
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(dataSource, { method: 'HEAD' })
expect(fetch.calls().length).toBe(1)
expect(fetch.lastUrl()).toBe(dataSource)
expect(fetch.lastOptions()).toEqual({ method: 'HEAD' })
expect((await db.getEmojiByShortcode('rofl'))).toBeFalsy()
expect((await db.getEmojiByShortcode('pineapple')).annotation).toBe('pineapple')
})
@ -133,8 +136,9 @@ describe('database second load and update', () => {
let db = new Database({ dataSource })
await db.ready()
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(dataSource, undefined)
expect(fetch.calls().length).toBe(1)
expect(fetch.lastUrl()).toBe(dataSource)
expect(fetch.lastOptions()).toBe(undefined)
expect((await db.getEmojiByShortcode('rofl')).annotation).toBe('rolling on the floor laughing')
expect(await db.getEmojiByShortcode('weary_cat')).toBeFalsy()
@ -151,9 +155,11 @@ describe('database second load and update', () => {
db = new Database({ dataSource })
await db.ready()
await db._lazyUpdate
expect(fetch).toHaveBeenCalledTimes(2)
expect(fetch).toHaveBeenLastCalledWith(dataSource, undefined)
expect(fetch).toHaveBeenNthCalledWith(1, dataSource, { method: 'HEAD' })
expect(fetch.calls().length).toBe(2)
expect(fetch.lastUrl()).toBe(dataSource)
expect(fetch.lastOptions()).toBe(undefined)
expect(fetch.calls().at(-2)[0]).toBe(dataSource)
expect(fetch.calls().at(-2)[1]).toEqual({ method: 'HEAD' })
expect((await db.getEmojiByShortcode('rofl'))).toBeFalsy()
expect((await db.getEmojiByShortcode('pineapple')).annotation).toBe('pineapple')
await db.close()
@ -164,9 +170,11 @@ describe('database second load and update', () => {
db = new Database({ dataSource })
await db.ready()
await db._lazyUpdate
expect(fetch).toHaveBeenCalledTimes(2)
expect(fetch).toHaveBeenLastCalledWith(dataSource, undefined)
expect(fetch).toHaveBeenNthCalledWith(1, dataSource, { method: 'HEAD' })
expect(fetch.calls().length).toBe(2)
expect(fetch.lastUrl()).toBe(dataSource)
expect(fetch.lastOptions()).toBe(undefined)
expect(fetch.calls().at(-2)[0]).toBe(dataSource)
expect(fetch.calls().at(-2)[1]).toEqual({ method: 'HEAD' })
expect((await db.getEmojiByShortcode('rofl'))).toBeFalsy()
expect((await db.getEmojiByShortcode('pineapple')).annotation).toBe('pineapple')
await db.delete()
@ -177,13 +185,13 @@ describe('database second load and update', () => {
const dataSource2 = 'http://localhost/will-change2.json'
// first time - data is v1
fetch.get(dataSource, () => new Response(JSON.stringify(truncatedEmoji), { headers: { ETag: 'W/xxx' } }))
fetch.head(dataSource, () => new Response(null, { headers: { ETag: 'W/xxx' } }))
mockGetAndHead(dataSource, truncatedEmoji, { headers: { ETag: 'W/xxx' } })
let db = new Database({ dataSource })
await db.ready()
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(dataSource, undefined)
expect(fetch.calls().length).toBe(1)
expect(fetch.lastUrl()).toBe(dataSource)
expect(fetch.lastOptions()).toBe(undefined)
expect((await db.getEmojiByShortcode('rofl')).annotation).toBe('rolling on the floor laughing')
expect(await db.getEmojiByShortcode('weary_cat')).toBeFalsy()
@ -195,31 +203,30 @@ describe('database second load and update', () => {
changedEmoji[roflIndex] = allEmoji.find(_ => _.annotation === 'pineapple') // replace rofl
// second time - update, data is v2
fetch.mockClear()
fetch.reset()
fetch.get(dataSource2, () => new Response(JSON.stringify(changedEmoji), { headers: { ETag: 'W/yyy' } }))
fetch.head(dataSource2, () => new Response(null, { headers: { ETag: 'W/yyy' } }))
mockGetAndHead(dataSource2, changedEmoji, { headers: { ETag: 'W/yyy' } })
db = new Database({ dataSource: dataSource2 })
await db.ready()
await db._lazyUpdate
expect(fetch).toHaveBeenCalledTimes(2)
expect(fetch).toHaveBeenLastCalledWith(dataSource2, undefined)
expect(fetch).toHaveBeenNthCalledWith(1, dataSource2, { method: 'HEAD' })
expect(fetch.calls().length).toBe(2)
expect(fetch.lastUrl()).toBe(dataSource2)
expect(fetch.lastOptions()).toBe(undefined)
expect(fetch.calls().at(-2)[0]).toBe(dataSource2)
expect(fetch.calls().at(-2)[1]).toEqual({ method: 'HEAD' })
expect((await db.getEmojiByShortcode('rofl'))).toBeFalsy()
expect((await db.getEmojiByShortcode('pineapple')).annotation).toBe('pineapple')
// third time - no update, data is v2
fetch.mockClear()
fetch.reset()
fetch.get(dataSource2, () => new Response(JSON.stringify(changedEmoji), { headers: { ETag: 'W/yyy' } }))
fetch.head(dataSource2, () => new Response(null, { headers: { ETag: 'W/yyy' } }))
mockGetAndHead(dataSource2, changedEmoji, { headers: { ETag: 'W/yyy' } })
db = new Database({ dataSource: dataSource2 })
await db.ready()
await db._lazyUpdate
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(dataSource2, { method: 'HEAD' })
expect(fetch.calls().length).toBe(1)
expect(fetch.lastUrl()).toBe(dataSource2)
expect(fetch.lastOptions()).toEqual({ method: 'HEAD' })
expect((await db.getEmojiByShortcode('rofl'))).toBeFalsy()
expect((await db.getEmojiByShortcode('pineapple')).annotation).toBe('pineapple')
@ -236,9 +243,11 @@ describe('database second load and update', () => {
db = new Database({ dataSource: otherSource })
await db.ready()
await tick(5) // the request is done asynchronously, so wait for it
expect(fetch).toHaveBeenCalledTimes(2)
expect(fetch).toHaveBeenNthCalledWith(1, otherSource, { method: 'HEAD' })
expect(fetch).toHaveBeenLastCalledWith(otherSource, undefined)
expect(fetch.calls().length).toBe(2)
expect(fetch.calls().at(-2)[0]).toBe(otherSource)
expect(fetch.calls().at(-2)[1]).toEqual({ method: 'HEAD' })
expect(fetch.lastUrl()).toBe(otherSource)
expect(fetch.lastOptions()).toBe(undefined)
await db.delete()
})
@ -251,10 +260,8 @@ describe('database second load and update', () => {
changedEmoji[roflIndex] = allEmoji.find(_ => _.annotation === 'pineapple') // replace rofl
// second time - update, data is v2
fetch.mockClear()
fetch.reset()
fetch.get(ALL_EMOJI, () => new Response(JSON.stringify(changedEmoji), { headers: { ETag: 'W/yyy' } }))
fetch.head(ALL_EMOJI, () => new Response(null, { headers: { ETag: 'W/yyy' } }))
mockGetAndHead(ALL_EMOJI, changedEmoji, { headers: { ETag: 'W/yyy' } })
// open two at once
const dbs = [

31
test/spec/mockFetch.js Normal file
View File

@ -0,0 +1,31 @@
// centralize all our fetch mocks in one place so we can have
// consistent timeouts, and smooth over some of the boilerplate
export function mockFetch (method, url, response, { headers, delay } = {}) {
let responseToUse
if (!response) {
responseToUse = null
} else if (typeof response === 'string') {
responseToUse = response
} else {
responseToUse = JSON.stringify(response)
}
fetch[method](
url,
() => new Response(responseToUse, { headers }),
// use a delay of 1 because it's more realistic than a fetch() that resolves in a microtask
{ delay: typeof delay === 'number' ? delay : 1 }
)
}
// convenience util for mocking a typical get and a head
export function mockGetAndHead (url, response, options = {}) {
mockFetch('get', url, response, options)
mockFetch('head', url, null, options)
}
export function mock500GetAndHead (url) {
fetch.get(url, { body: null, status: 500 })
fetch.head(url, { body: null, status: 500 })
}

View File

@ -31,8 +31,9 @@ describe('attributes tests', () => {
document.body.appendChild(picker)
await tick(20)
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(FR_EMOJI, undefined)
expect(fetch.calls().length).toBe(1)
expect(fetch.lastUrl()).toBe(FR_EMOJI)
expect(fetch.lastOptions()).toBe(undefined)
expect(picker.locale).toEqual('fr')
expect(picker.dataSource).toEqual(FR_EMOJI)
@ -169,8 +170,9 @@ describe('attributes tests', () => {
expect(getByRole(picker.shadowRoot, 'button', { name: /Choose a skin tone/ }).innerHTML)
.toContain('✌')
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(ALL_EMOJI, undefined)
expect(fetch.calls().length).toBe(1)
expect(fetch.lastUrl()).toBe(ALL_EMOJI)
expect(fetch.lastOptions()).toBe(undefined)
document.body.removeChild(div)
await tick(20)

View File

@ -16,8 +16,9 @@ describe('constructor', () => {
await waitFor(() => expect(getByRole(container, 'menuitem', { name: /😀/ })).toBeVisible())
expect(getByRole(container, 'menuitem', { name: /😀/ })).toBeVisible()
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(DEFAULT_DATA_SOURCE, undefined)
expect(fetch.calls().length).toBe(1)
expect(fetch.lastUrl()).toBe(DEFAULT_DATA_SOURCE)
expect(fetch.lastOptions()).toBe(undefined)
document.body.removeChild(picker)
await tick(20)

View File

@ -13,6 +13,7 @@ import enI18n from '../../../src/picker/i18n/en'
import Database from '../../../src/database/Database'
import { DEFAULT_SKIN_TONE_EMOJI } from '../../../src/picker/constants'
import { DEFAULT_DATA_SOURCE } from '../../../src/database/constants'
import { mockGetAndHead } from '../mockFetch.js'
const { type } = userEvent
// Workaround for clear() not working in shadow roots: https://github.com/testing-library/user-event/issues/1143
@ -53,8 +54,9 @@ describe('element tests', () => {
test('changing locale/dataSource prop causes only one network request', async () => {
await tick(120)
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(ALL_EMOJI, undefined)
expect(fetch.calls().length).toBe(1)
expect(fetch.lastUrl()).toBe(ALL_EMOJI)
expect(fetch.lastOptions()).toBe(undefined)
await type(getByRole(container, 'combobox'), 'monkey face')
await waitFor(() => expect(getAllByRole(container, 'option')).toHaveLength(1), {
timeout: 2000
@ -64,8 +66,9 @@ describe('element tests', () => {
picker.locale = 'fr'
picker.dataSource = FR_EMOJI
await tick(120)
expect(fetch).toHaveBeenCalledTimes(2)
expect(fetch).toHaveBeenLastCalledWith(FR_EMOJI, undefined)
expect(fetch.calls().length).toBe(2)
expect(fetch.lastUrl()).toBe(FR_EMOJI)
expect(fetch.lastOptions()).toBe(undefined)
await clear(getByRole(container, 'combobox'))
await type(getByRole(container, 'combobox'), 'singe tête')
await waitFor(() => expect(getAllByRole(container, 'option')).toHaveLength(1))
@ -74,8 +77,9 @@ describe('element tests', () => {
test('changing locale/dataSource attr causes only one network request', async () => {
await tick(120)
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(ALL_EMOJI, undefined)
expect(fetch.calls().length).toBe(1)
expect(fetch.lastUrl()).toBe(ALL_EMOJI)
expect(fetch.lastOptions()).toBe(undefined)
await type(getByRole(container, 'combobox'), 'monkey face')
await waitFor(() => expect(getAllByRole(container, 'option')).toHaveLength(1), {
timeout: 2000
@ -85,8 +89,9 @@ describe('element tests', () => {
picker.setAttribute('locale', 'fr')
picker.setAttribute('data-source', FR_EMOJI)
await tick(120)
expect(fetch).toHaveBeenCalledTimes(2)
expect(fetch).toHaveBeenLastCalledWith(FR_EMOJI, undefined)
expect(fetch.calls().length).toBe(2)
expect(fetch.lastUrl()).toBe(FR_EMOJI)
expect(fetch.lastOptions()).toBe(undefined)
await clear(getByRole(container, 'combobox'))
await type(getByRole(container, 'combobox'), 'singe tête')
await waitFor(() => expect(getAllByRole(container, 'option')).toHaveLength(1))
@ -119,8 +124,7 @@ describe('element tests', () => {
describe('defaults test', () => {
beforeEach(() => {
fetch.get(DEFAULT_DATA_SOURCE, () => new Response(JSON.stringify(truncatedEmoji), { headers: { ETag: 'W/aaa' } }))
fetch.head(DEFAULT_DATA_SOURCE, () => new Response(null, { headers: { ETag: 'W/aaa' } }))
mockGetAndHead(DEFAULT_DATA_SOURCE, truncatedEmoji, { headers: { ETag: 'W/aaa' } })
})
afterEach(basicAfterEach)

View File

@ -1,15 +1,16 @@
import { jest } from '@jest/globals'
import { vi } from 'vitest'
import Picker from '../../../src/picker/PickerElement'
import { ALL_EMOJI, basicAfterEach, basicBeforeEach, tick, truncatedEmoji } from '../shared'
import Database from '../../../src/database/Database'
import { getByRole, waitFor } from '@testing-library/dom'
import { mock500GetAndHead, mockGetAndHead } from '../mockFetch.js'
describe('errors', () => {
let errorSpy
beforeEach(async () => {
await basicBeforeEach()
errorSpy = jest.spyOn(global.console, 'error').mockImplementation()
errorSpy = vi.spyOn(global.console, 'error').mockImplementation(() => undefined)
await tick(40)
})
afterEach(async () => {
@ -19,7 +20,6 @@ describe('errors', () => {
await tick(40)
})
// seems not possible to do
test('throws error when setting the database', async () => {
const picker = new Picker({ dataSource: ALL_EMOJI, locale: 'en' })
document.body.appendChild(picker)
@ -32,12 +32,10 @@ describe('errors', () => {
await tick(20)
})
// can't seem to get jest to ignore these expected errors
test('offline shows an error', async () => {
const dataSource = 'error.json'
fetch.get(dataSource, { body: null, status: 500 })
fetch.head(dataSource, { body: null, status: 500 })
mock500GetAndHead(dataSource)
const picker = new Picker({ dataSource })
const container = picker.shadowRoot
@ -57,10 +55,7 @@ describe('errors', () => {
test('slow networks show "Loading"', async () => {
const dataSource = 'slow.json'
fetch.get(dataSource, () => new Response(JSON.stringify(truncatedEmoji), { headers: { ETag: 'W/slow' } }),
{ delay: 1500 })
fetch.head(dataSource, () => new Response(null, { headers: { ETag: 'W/slow' } }),
{ delay: 1500 })
mockGetAndHead(dataSource, truncatedEmoji, { headers: { ETag: 'W/slow' }, delay: 1500 })
const picker = new Picker({ dataSource })
const container = picker.shadowRoot

View File

@ -8,6 +8,7 @@ import allData from 'emoji-picker-element-data/en/emojibase/data.json'
import { MOST_COMMONLY_USED_EMOJI } from '../../../src/picker/constants'
import { uniqBy } from '../../../src/shared/uniqBy'
import { groups } from '../../../src/picker/groups'
import { mockGetAndHead } from '../mockFetch.js'
const dataSource = 'with-favs.json'
@ -23,8 +24,7 @@ describe('Favorites UI', () => {
...allData.filter(_ => MOST_COMMONLY_USED_EMOJI.includes(_.emoji))
], _ => _.emoji)
fetch.get(dataSource, () => new Response(JSON.stringify(dataWithFavorites), { headers: { ETag: 'W/favs' } }))
fetch.head(dataSource, () => new Response(null, { headers: { ETag: 'W/favs' } }))
mockGetAndHead(dataSource, dataWithFavorites, { headers: { ETag: 'W/favs' } })
picker = new Picker({ dataSource, locale: 'en' })
document.body.appendChild(picker)

View File

@ -58,7 +58,26 @@ describe('framework', () => {
expect(node.outerHTML).toBe('<div><span>foo</span></div>')
})
test('render two dynamic expressions inside the same element', () => {
test('dynamic expression with whitespace around it - minifier should be working', () => {
const state = { name: 'foo' }
const { html } = createFramework(state)
let node
const render = () => {
node = html`<div> ${state.name}\t\n</div>`
}
render()
expect(node.outerHTML).toBe('<div>foo</div>')
state.name = 'baz'
render()
expect(node.outerHTML).toBe('<div>baz</div>')
})
// Framework no longer supports this since we switched from HTML comments to text nodes
test.skip('render two dynamic expressions inside the same element', () => {
const state = { name1: 'foo', name2: 'bar' }
const { html } = createFramework(state)
@ -77,7 +96,8 @@ describe('framework', () => {
expect(node.outerHTML).toBe('<div>bazquux</div>')
})
test('render a mix of dynamic and static text nodes in the same element', () => {
// Framework no longer supports this since we switched from HTML comments to text nodes
test.skip('render a mix of dynamic and static text nodes in the same element', () => {
const state = { name1: 'foo', name2: 'bar' }
const { html } = createFramework(state)

View File

@ -1,4 +1,4 @@
import { jest } from '@jest/globals'
import { vi } from 'vitest'
import { basicAfterEach, basicBeforeEach, tick } from '../shared'
import Picker from '../../../src/picker/PickerElement'
import { getByRole, waitFor } from '@testing-library/dom'
@ -20,8 +20,9 @@ describe('lifecycle', () => {
await waitFor(() => expect(getByRole(container, 'menuitem', { name: /😀/ })).toBeVisible())
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(DEFAULT_DATA_SOURCE, undefined)
expect(fetch.calls().length).toBe(1)
expect(fetch.lastUrl()).toBe(DEFAULT_DATA_SOURCE)
expect(fetch.lastOptions()).toBe(undefined)
document.body.removeChild(picker)
await tick(40)
@ -30,8 +31,9 @@ describe('lifecycle', () => {
await waitFor(() => expect(getByRole(container, 'menuitem', { name: /😀/ })).toBeVisible())
// fetch is called once again after re-insertion
expect(fetch).toHaveBeenCalledTimes(2)
expect(fetch).toHaveBeenLastCalledWith(DEFAULT_DATA_SOURCE, { method: 'HEAD' })
expect(fetch.calls().length).toBe(2)
expect(fetch.lastUrl()).toBe(DEFAULT_DATA_SOURCE)
expect(fetch.lastOptions()).toEqual({ method: 'HEAD' })
document.body.removeChild(picker)
await tick(60)
@ -45,7 +47,7 @@ describe('lifecycle', () => {
await waitFor(() => expect(getByRole(container, 'menuitem', { name: /😀/ })).toBeVisible())
const spy = jest.spyOn(picker.database, 'close')
const spy = vi.spyOn(picker.database, 'close')
document.body.removeChild(picker)
await tick(60)
@ -64,8 +66,9 @@ describe('lifecycle', () => {
await tick(60)
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(DEFAULT_DATA_SOURCE, undefined)
expect(fetch.calls().length).toBe(1)
expect(fetch.lastUrl()).toBe(DEFAULT_DATA_SOURCE)
expect(fetch.lastOptions()).toBe(undefined)
expect(Object.keys(openIndexedDBRequests).length).toBe(0) // no open IDB connections
})
@ -78,8 +81,9 @@ describe('lifecycle', () => {
await tick(120)
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(DEFAULT_DATA_SOURCE, undefined)
expect(fetch.calls().length).toBe(1)
expect(fetch.lastUrl()).toBe(DEFAULT_DATA_SOURCE)
expect(fetch.lastOptions()).toBe(undefined)
expect(Object.keys(openIndexedDBRequests).length).toBe(0) // no open IDB connections
})
@ -105,8 +109,9 @@ describe('lifecycle', () => {
document.body.appendChild(picker)
await tick(40)
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(DEFAULT_DATA_SOURCE, undefined)
expect(fetch.calls().length).toBe(1)
expect(fetch.lastUrl()).toBe(DEFAULT_DATA_SOURCE)
expect(fetch.lastOptions()).toBe(undefined)
await expect(() => (
expect(getByRole(picker.shadowRoot, 'option', { name: /😀/ })).toBeVisible()
))
@ -116,7 +121,7 @@ describe('lifecycle', () => {
await tick(40)
expect(fetch).toHaveBeenCalledTimes(1) // fetch is not called again because no re-render
expect(fetch.calls().length).toBe(1) // fetch is not called again because no re-render
await expect(() => (
expect(getByRole(picker.shadowRoot, 'option', { name: /😀/ })).toBeVisible()
))
@ -132,8 +137,9 @@ describe('lifecycle', () => {
document.body.appendChild(picker)
await tick(40)
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(DEFAULT_DATA_SOURCE, undefined)
expect(fetch.calls().length).toBe(1)
expect(fetch.lastUrl()).toBe(DEFAULT_DATA_SOURCE)
expect(fetch.lastOptions()).toBe(undefined)
await expect(() => (
expect(getByRole(picker.shadowRoot, 'option', { name: /😀/ })).toBeVisible()
))
@ -144,8 +150,9 @@ describe('lifecycle', () => {
await tick(40)
expect(fetch).toHaveBeenCalledTimes(2) // fetch is called again due to re-render
expect(fetch).toHaveBeenLastCalledWith(DEFAULT_DATA_SOURCE, { method: 'HEAD' }) // cached, so does a HEAD
expect(fetch.calls().length).toBe(2) // fetch is called again due to re-render
expect(fetch.lastUrl()).toBe(DEFAULT_DATA_SOURCE)
expect(fetch.lastOptions()).toEqual({ method: 'HEAD' }) // cached, so does a HEAD
await expect(() => (
expect(getByRole(picker.shadowRoot, 'option', { name: /😀/ })).toBeVisible()
))
@ -166,8 +173,9 @@ describe('lifecycle', () => {
await tick(40)
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(DEFAULT_DATA_SOURCE, undefined)
expect(fetch.calls().length).toBe(1)
expect(fetch.lastUrl()).toBe(DEFAULT_DATA_SOURCE)
expect(fetch.lastOptions()).toBe(undefined)
await expect(() => (
expect(getByRole(picker.shadowRoot, 'option', { name: /😀/ })).toBeVisible()
))

View File

@ -23,8 +23,9 @@ describe('properties', () => {
await tick(40)
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(FR_EMOJI, undefined)
expect(fetch.calls().length).toBe(1)
expect(fetch.lastUrl()).toBe(FR_EMOJI)
expect(fetch.lastOptions()).toBe(undefined)
expect(picker.locale).toEqual('fr')
expect(picker.dataSource).toEqual(FR_EMOJI)
@ -43,8 +44,9 @@ describe('properties', () => {
await tick(40)
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(FR_EMOJI, undefined)
expect(fetch.calls().length).toBe(1)
expect(fetch.lastUrl()).toBe(FR_EMOJI)
expect(fetch.lastOptions()).toBe(undefined)
expect(picker.locale).toEqual('en')
expect(picker.dataSource).toEqual(FR_EMOJI)
@ -63,8 +65,9 @@ describe('properties', () => {
await tick(40)
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(DEFAULT_DATA_SOURCE, undefined)
expect(fetch.calls().length).toBe(1)
expect(fetch.lastUrl()).toBe(DEFAULT_DATA_SOURCE)
expect(fetch.lastOptions()).toBe(undefined)
expect(picker.locale).toEqual('fr')
expect(picker.dataSource).toEqual(DEFAULT_DATA_SOURCE)

View File

@ -29,7 +29,7 @@ describe('upgrade tests', () => {
await tick(20)
expect(fetch).not.toHaveBeenCalled()
expect(fetch.calls().length).toBe(0)
await import('../../../src/picker/PickerElement')
@ -37,8 +37,9 @@ describe('upgrade tests', () => {
const container = picker.shadowRoot
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(FR_EMOJI, undefined)
expect(fetch.calls().length).toBe(1)
expect(fetch.lastUrl()).toBe(FR_EMOJI)
expect(fetch.lastOptions()).toBe(undefined)
expect(getByRole(container, 'button', { name: /Choose a skin tone/ }).innerHTML).toContain('👍')

View File

@ -2,6 +2,7 @@ import allEmoji from 'emoji-picker-element-data/en/emojibase/data.json'
import frEmoji from 'emoji-picker-element-data/fr/cldr/data.json'
import allEmojibaseV5Emoji from 'emojibase-data/en/data.json'
import { DEFAULT_DATA_SOURCE } from '../../src/database/constants'
import { mockFetch, mockGetAndHead } from './mockFetch.js'
export function truncateEmoji (allEmoji) {
// just take the first few emoji from each category, or else it takes forever to insert
@ -36,25 +37,14 @@ export const EMOJIBASE_V5 = 'http://localhost/emojibase'
export const WITH_ARRAY_SKIN_TONES = 'http://localhost/with-array-skin-tones'
export function basicBeforeEach () {
fetch
.get(ALL_EMOJI, () => new Response(JSON.stringify(truncatedEmoji), {
headers: { ETag: 'W/xxx' }
}))
.head(ALL_EMOJI, () => new Response(null, {
headers: { ETag: 'W/xxx' }
}))
.get(ALL_EMOJI_NO_ETAG, truncatedEmoji)
.head(ALL_EMOJI_NO_ETAG, () => new Response(null))
.get(ALL_EMOJI_MISCONFIGURED_ETAG, () => new Response(JSON.stringify(truncatedEmoji), {
headers: { ETag: 'W/xxx' }
}))
.head(ALL_EMOJI_MISCONFIGURED_ETAG, () => new Response(null))
.get(DEFAULT_DATA_SOURCE, () => new Response(JSON.stringify(truncatedEmoji), { headers: { ETag: 'W/def' } }))
.head(DEFAULT_DATA_SOURCE, () => new Response(null, { headers: { ETag: 'W/def' } }))
mockGetAndHead(ALL_EMOJI, truncatedEmoji, { headers: { ETag: 'W/xxx' } })
mockGetAndHead(ALL_EMOJI_NO_ETAG, truncatedEmoji)
mockGetAndHead(DEFAULT_DATA_SOURCE, truncatedEmoji, { headers: { ETag: 'W/def' } })
mockFetch('get', ALL_EMOJI_MISCONFIGURED_ETAG, truncatedEmoji, { headers: { ETag: 'W/xxx' } })
mockFetch('head', ALL_EMOJI_MISCONFIGURED_ETAG, null)
}
export async function basicAfterEach () {
fetch.mockClear()
fetch.reset()
await tick(20)
}
@ -66,8 +56,7 @@ export async function tick (times = 1) {
}
export function mockFrenchDataSource () {
fetch.get(FR_EMOJI, () => new Response(JSON.stringify(truncatedFrEmoji), { headers: { ETag: 'W/zzz' } }))
fetch.head(FR_EMOJI, () => new Response(null, { headers: { ETag: 'W/zzz' } }))
mockGetAndHead(FR_EMOJI, truncatedFrEmoji, { headers: { ETag: 'W/zzz' } })
}
export function mockDataSourceWithNoShortcodes () {
@ -76,22 +65,16 @@ export function mockDataSourceWithNoShortcodes () {
delete res.shortcodes
return res
})
fetch.get(NO_SHORTCODES, () => new Response(JSON.stringify(noShortcodeEmoji), { headers: { ETag: 'W/noshort' } }))
fetch.head(NO_SHORTCODES, () => new Response(null, { headers: { ETag: 'W/noshort' } }))
mockGetAndHead(NO_SHORTCODES, noShortcodeEmoji, { headers: { ETag: 'W/noshort' } })
}
export function mockEmojibaseV5DataSource () {
fetch.get(EMOJIBASE_V5, () => new Response(JSON.stringify(emojibaseV5Emoji), { headers: { ETag: 'W/emojibase' } }))
fetch.head(EMOJIBASE_V5, () => new Response(null, { headers: { ETag: 'W/emojibase' } }))
mockGetAndHead(EMOJIBASE_V5, emojibaseV5Emoji, { headers: { ETag: 'W/emojibase' } })
}
export function mockDataSourceWithArraySkinTones () {
const emojis = JSON.parse(JSON.stringify(truncatedEmoji))
emojis.push(allEmoji.find(_ => _.annotation === 'people holding hands')) // has two skin tones, one for each person
fetch
.get(WITH_ARRAY_SKIN_TONES, () => (
new Response(JSON.stringify(emojis), { headers: { ETag: 'W/noshort' } }))
)
.head(WITH_ARRAY_SKIN_TONES, () => new Response(null, { headers: { ETag: 'W/noshort' } }))
mockGetAndHead(WITH_ARRAY_SKIN_TONES, emojis, { headers: { ETag: 'W/noshort' } })
}

30
vitest.config.js Normal file
View File

@ -0,0 +1,30 @@
import { defineConfig } from 'vitest/config'
import { minifyHtmlLiteralsRollupPlugin } from './config/minifyHtmlLiteralsRollupPlugin.js'
import { buildStylesRollupPlugin } from './config/buildStylesRollupPlugin.js'
export default defineConfig({
plugins: [
minifyHtmlLiteralsRollupPlugin(),
buildStylesRollupPlugin()
],
test: {
globals: true,
environment: 'jsdom',
setupFiles: [
'./config/vitest.setup.js'
],
testTimeout: 60000,
coverage: {
provider: 'istanbul',
include: [
'src/'
],
thresholds: {
statements: 100,
branches: 100,
functions: 100,
lines: 100
}
}
}
})

8560
yarn.lock

File diff suppressed because it is too large Load Diff