Compare commits
18 Commits
Author | SHA1 | Date |
---|---|---|
Nolan Lawson | 7a135f3368 | |
Nolan Lawson | 1cd4b9da68 | |
Nolan Lawson | 028f4dc8ed | |
Nolan Lawson | 3d84cf384e | |
Éric Le Maître | e52867681a | |
dependabot[bot] | 9211adedfa | |
dependabot[bot] | 320414bcb7 | |
Nolan Lawson | 6a0f4a25ad | |
Nolan Lawson | 6456dbf8a4 | |
Nolan Lawson | c067cbab29 | |
Nolan Lawson | 1ae4e30e91 | |
Nolan Lawson | ce950ff740 | |
Nolan Lawson | ff88212004 | |
Nolan Lawson | 2a57ba18e8 | |
Nolan Lawson | 55872ba996 | |
Nolan Lawson | 15bca2197f | |
Nolan Lawson | cc1f64d23e | |
Nolan Lawson | 7365322a5f |
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1 @@
|
|||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
./node_modules/.bin/lint-staged
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
nodeLinker: node-modules
|
19
CHANGELOG.md
19
CHANGELOG.md
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
})
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)))
|
||||
})
|
|
@ -1,9 +0,0 @@
|
|||
import { minifyHTMLLiterals } from 'minify-html-literals'
|
||||
|
||||
export default {
|
||||
processAsync (source, fileName) {
|
||||
return minifyHTMLLiterals(source, {
|
||||
fileName
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)))
|
||||
})
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
70
package.json
70
package.json
|
@ -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"
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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)
|
||||
}),
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { terser } from 'rollup-plugin-terser'
|
||||
import terser from '@rollup/plugin-terser'
|
||||
|
||||
export default {
|
||||
input: './index.js',
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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' })
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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 })
|
||||
|
||||
|
|
|
@ -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 })
|
||||
|
||||
|
|
|
@ -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 })
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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 })
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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('👍')
|
||||
|
||||
|
|
|
@ -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' } })
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
Loading…
Reference in New Issue