diff --git a/.circleci/config.yml b/.circleci/config.yml index 8a5b04e20cae..6e1baf8dad8d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,7 +6,7 @@ version: 2.1 executors: maskbook_node: docker: - - image: circleci/node:13.2.0 + - image: circleci/node:13.2.0-browsers working_directory: ~/repo commands: restore_workspace: @@ -22,13 +22,13 @@ jobs: - restore_cache: name: Restore Yarn Package Cache keys: - - v1-maskbook-cache + - v2-maskbook-cache - run: name: Install Dependencies command: yarn install --link-duplicates --frozen-lockfile - save_cache: name: Save Yarn Package Cache - key: v1-maskbook-cache + key: v2-maskbook-cache paths: - ~/.cache/yarn - persist_to_workspace: @@ -41,7 +41,7 @@ jobs: - restore_workspace - run: name: 'TypeScript type check' - command: yarn tsc -p tsconfig_cjs.json --noEmit + command: yarn tsc --noEmit eslint: executor: maskbook_node steps: @@ -82,6 +82,23 @@ jobs: path: reports/junit - store_artifacts: path: coverage + e2e: + executor: maskbook_node + steps: + - restore_workspace + - run: + name: Setup E2E + command: | + mkdir -p ./screenshots + yarn build:e2e + - run: + name: Jest E2E + command: yarn test:e2e + - run: + name: Teardown E2E + command: rm -rf ./build + - store_artifacts: + path: screenshots build: executor: maskbook_node steps: @@ -91,7 +108,7 @@ jobs: command: sudo apt-get install zip - run: name: Build Maskbook - command: node ./scripts/ci-build.js + command: yarn build-ci - store_artifacts: path: Maskbook.base.zip destination: /Maskbook.base.zip @@ -184,6 +201,10 @@ workflows: requires: # Fail quick with type error. - typescript + - e2e: + requires: + # Avoid accessing build/ folder at the same time + - build - publish-github-release: requires: - build diff --git a/.commitlintrc.json b/.commitlintrc.json new file mode 100644 index 000000000000..f4fbb7ddefac --- /dev/null +++ b/.commitlintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["@commitlint/config-conventional"] +} diff --git a/.gitignore b/.gitignore index 3d142b8a8c40..631edc61dea8 100644 --- a/.gitignore +++ b/.gitignore @@ -14,11 +14,13 @@ public/polyfill # testing /coverage +/junit.xml # production /build /dist /storybook-static +/Maskbook.*.zip # misc .DS_Store @@ -31,6 +33,9 @@ public/polyfill # contracts /contracts +# e2e +/screenshots + npm-debug.log* yarn-debug.log* yarn-error.log* diff --git a/.vscode/settings.json b/.vscode/settings.json index 5fc89a8723b1..2711d07db127 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "typescript.tsdk": "node_modules/typescript/lib", + "editor.formatOnSave": true, "i18n-ally.sourceLanguage": "en", "i18n-ally.displayLanguage": "zh", "i18n-ally.localesPaths": "src/_locales", diff --git a/commitlint.config.js b/commitlint.config.js deleted file mode 100644 index 3e16e7f1b100..000000000000 --- a/commitlint.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - extends: ['@commitlint/config-conventional'], -} diff --git a/config-overrides/manifest.overrides.js b/config-overrides/manifest.overrides.js index 9ba70f8141a6..4299a5cb4b34 100644 --- a/config-overrides/manifest.overrides.js +++ b/config-overrides/manifest.overrides.js @@ -23,7 +23,9 @@ function chromium(manifest) {} /** * @param {typeof base} manifest */ -function WKWebview(manifest) {} +function WKWebview(manifest) { + manifest['iOS-injected-scripts'] = ['js/injected-script.js'] +} function development(manifest, target) { manifest.key = // ID:jkoeaghipilijlahjplgbfiocjhldnap 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoz51rhO1w+wD0EKZJEFJaSMkIcIj0qRadfi0tqcl5nbpuJAsafvLe3MaTbW9LhbixTg9PHybO3tlYUFJrZgUuZlEvt2T6SKIu6Rs9e9B3/brNQG3+hCHudbZkq2WG2IzO44dglrs24bRp/pV5oIif0bLuwrzvYsPQ6hgSp+5gc4pg0LEJPLFp01fbORDknWt8suJmEMz7S0O5+u13+34NvxYzUNeLJF9gYrd4zzrAFYITDEYcqr0OMZvVrKz7IkJasER1uJyoGj4gFJeXNGE8y4Sqb150wBju70lKNKlNevWDRJKasG9CjagAD2+BAfqNyltn7KwK7jAyL1w6d6mOwIDAQAB' diff --git a/e2e/dashboard.spec.ts b/e2e/dashboard.spec.ts new file mode 100644 index 000000000000..ada7e986e389 --- /dev/null +++ b/e2e/dashboard.spec.ts @@ -0,0 +1,16 @@ +describe('dashboard - setup a account', () => { + beforeAll(async () => { + await page.goto('chrome-extension://jkoeaghipilijlahjplgbfiocjhldnap/index.html') + }) + + it('render welcome page', async () => { + const setupButton = await page.waitFor('a[href="#/initialize/1s"]') + expect(await setupButton.evaluate(e => e.textContent.toLowerCase())).toBe('set up') + }) + + it('take a screenshot', async () => { + await page.screenshot({ + path: './screenshots/welcome.png', + }) + }) +}) diff --git a/jest-e2e.config.js b/jest-e2e.config.js new file mode 100644 index 000000000000..8f2d1f76a58e --- /dev/null +++ b/jest-e2e.config.js @@ -0,0 +1,5 @@ +const path = require('path') +module.exports = { + preset: 'jest-puppeteer', + testRegex: ['/e2e/.*\\.[jt]sx?$'], +} diff --git a/jest-puppeteer.config.js b/jest-puppeteer.config.js new file mode 100644 index 000000000000..e5cb24b0494a --- /dev/null +++ b/jest-puppeteer.config.js @@ -0,0 +1,16 @@ +const path = require('path') +const extPath = path.join(__dirname, './build') +module.exports = { + launch: { + dumpio: true, + headless: false, + args: [ + '--no-sandbox', + '--disable-infobars', + `--disable-extensions-except=${extPath}`, + `--load-extension=${extPath}`, + ], + }, + browser: 'chromium', + browserContext: 'default', +} diff --git a/jest.config.js b/jest.config.js index 24ff4fd091ed..ea8a8527be19 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,6 +1,7 @@ const path = require('path') module.exports = { + testRegex: ['/__tests__/.*\\.[jt]sx?$'], preset: 'ts-jest', testEnvironment: 'jest-environment-jsdom-fourteen', globals: { @@ -9,6 +10,7 @@ module.exports = { }, }, globalSetup: path.join(__dirname, './scripts/jest-global-setup'), + globalTeardown: path.join(__dirname, './scripts/jest-global-teardown'), setupFiles: [ require.resolve('jest-webextension-mock'), require.resolve('fake-indexeddb/auto'), diff --git a/package.json b/package.json index 8d3ce8e831b8..60c97b86aee7 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,23 @@ { "name": "maskbook", - "version": "1.10.9", + "version": "1.10.10", "private": true, "license": "AGPL-3.0-or-later", "scripts": { "build": "webpack --mode production", + "build-ci": "ts-node scripts/ci-build.ts", "build-storybook": "build-storybook -s public --quiet", "build:base": "webpack --mode production", "build:chromium": "webpack --mode production --chromium", "build:firefox": "webpack --mode production --firefox", "build:gecko": "webpack --mode production --firefox-gecko", "build:ios": "webpack --mode production --wk-webview", + "build:e2e": "webpack --mode production --e2e", "compile:contracts": "./scripts/contracts.sh", - "install": "node scripts/postinstall.js", + "install": "ts-node scripts/postinstall.ts", "lint": "eslint src/ --ext .ts,.tsx --fix", "lint:report": "eslint src/ --ext .ts,.tsx --format junit -o reports/junit/eslint-results.xml", - "lint:typecoverage": "type-coverage -p tsconfig_cjs.json --strict --ignore-catch --detail --at-least 98.5", + "lint:typecoverage": "type-coverage --strict --ignore-catch --detail --at-least 98.5", "start": "webpack-dev-server --mode development", "start:android": "webpack-dev-server --firefox-android --mode development", "start:chromium": "webpack-dev-server --chromium --mode development", @@ -24,8 +26,9 @@ "start:ios": "webpack-dev-server --wk-webview --mode development", "storybook": "start-storybook -p 9009 -s public --quiet", "test": "jest --watch", - "test:ci": "jest --ci --collectCoverage=true --reporters=default --reporters=jest-junit", - "uninstall": "node scripts/postinstall.js" + "test:ci": "jest --ci --collectCoverage=true --reporters=default --reporters=jest-junit -w 1", + "test:e2e": "jest --runInBand --config=jest-e2e.config.js", + "uninstall": "ts-node scripts/postinstall.ts" }, "husky": { "hooks": { @@ -35,7 +38,7 @@ }, "lint-staged": { "*.{ts,tsx,js,jsx}": [ - "npm run lint" + "eslint --ext .ts,.tsx --fix" ] }, "dependencies": { @@ -73,13 +76,13 @@ "jsonwebtoken": "^8.5.1", "jsqr": "^1.2.0", "lodash-es": "^4.17.11", - "node-stego": "^0.10.0", + "node-stego": "^0.10.1", "notistack": "^0.9.7", "openzeppelin-solidity": "^2.4.0", "pvtsutils": "^1.0.9", "qrcode": "^1.4.4", - "react": "^16.12.0", - "react-dom": "^16.12.0", + "react": "^16.13.0", + "react-dom": "^16.13.0", "react-i18next": "^11.3.2", "react-router-dom": "^5.0.0", "react-use": "^13.26.3", @@ -115,7 +118,10 @@ "@types/enzyme": "^3.10.5", "@types/enzyme-adapter-react-16": "^1.0.6", "@types/jest": "^25.1.2", - "@typescript-eslint/eslint-plugin": "^2.20.0", + "@typescript-eslint/eslint-plugin": "^2.22.0", + "@types/expect-puppeteer": "^4.4.0", + "@types/jest-environment-puppeteer": "^4.3.1", + "@types/puppeteer": "^2.0.0", "@typescript-eslint/parser": "^2.19.2", "asmcrypto.js": "^2.3.2", "awesome-typescript-loader": "^5.2.1", @@ -130,6 +136,7 @@ "eslint-plugin-prettier": "^3.1.2", "eslint-plugin-react-hooks": "^2.3.0", "esm": "^3.2.25", + "expect-puppeteer": "^4.4.0", "fake-indexeddb": "^3.0.0", "fork-ts-checker-notifier-webpack-plugin": "^2.0.0", "fork-ts-checker-webpack-plugin": "^4.0.4", @@ -139,13 +146,14 @@ "jest-esm-transformer": "^1.0.0", "jest-junit": "^10.0.0", "jest-webextension-mock": "^3.5.0", + "jest-puppeteer": "^4.4.0", "lint-staged": "^10.0.7", "npm-run-all": "^4.1.5", "prettier": "^1.19.1", - "puppeteer-core": "^2.1.1", + "puppeteer": "^2.1.1", "react-devtools": "^4.4.0", "react-docgen-typescript-loader": "^3.6.0", - "react-test-renderer": "^16.12.0", + "react-test-renderer": "^16.13.0", "storybook-addon-figma": "^0.1.0", "storybook-addon-i18n": "^5.1.13", "storybook-addon-material-ui": "^0.9.0-alpha.21", @@ -159,7 +167,7 @@ "typechain-target-web3-v1": "^1.0.4", "typescript": "^3.7.5", "web-ext": "^4.1.0", - "webpack": "^4.41.6", + "webpack": "^4.42.0", "webpack-cli": "^3.3.10", "webpack-dev-server": "^3.10.3", "webpack-extension-manifest-plugin": "^0.5.0", diff --git a/scripts/ci-build.js b/scripts/ci-build.js deleted file mode 100644 index d9fe0181ea74..000000000000 --- a/scripts/ci-build.js +++ /dev/null @@ -1,72 +0,0 @@ -const { spawn } = require('./spawn') -const path = require('path') - -const base = path.join(__dirname, '../') -process.chdir(base) -async function main() { - const prepareCommands = getCommands(` - `) - for (const command of prepareCommands) { - await spawn(command) - } - const currentBranch = (await getCurrentGitBranchName()).toLowerCase() - for (const command of getCommands(buildTypes(currentBranch))) { - console.log('executing', command) - await spawn(command) - } - process.exit(0) -} -main().catch(e => { - console.error(e) - process.exit(1) -}) - -/** - * @param {string} branchName - */ -function buildTypes(branchName) { - if (branchName.match(/full/) || branchName === 'master') - return getBuildCommand(['base', 'chromium', 'firefox', 'gecko', 'iOS']) - if (branchName.match(/ios/)) return getBuildCommand(['iOS']) - if (branchName.match(/android|gecko/)) return getBuildCommand(['firefox', 'gecko']) - return getBuildCommand(['base', 'chromium', 'firefox']) -} - -/** - * @param {('base' | 'iOS' | 'chromium' | 'firefox' | 'gecko')[]} platforms - */ -function getBuildCommand(platforms) { - return platforms - .sort() - .map(generateCommand) - .join('\n') - /** @param {('base' | 'iOS' | 'chromium' | 'firefox' | 'gecko')} type */ - function generateCommand(type) { - if (type === 'chromium' && platforms.indexOf('base') !== -1) { - // chromium doesn't have it's own changes yet. - // just copying base version is acceptable - return 'cp Maskbook.base.zip Maskbook.chromium.zip' - } - return ` - echo "Building for target ${type}" - yarn build:${type.toLowerCase()} - bash -c "cd build && zip -r ../Maskbook.${type}.zip ./*" - rm -rf build - ` - } -} -async function getCurrentGitBranchName() { - const gitcmd = getCommands(`git rev-parse --abbrev-ref HEAD`)[0] - const branch = await spawn(gitcmd, null, { stdio: 'pipe' }) - return branch.split('\n')[0] -} - -/** - * @param {string} string - */ -function getCommands(string) { - return string - .split('\n') - .map(x => x.trimLeft()) - .filter(x => x && !x.startsWith('#')) -} diff --git a/scripts/ci-build.ts b/scripts/ci-build.ts new file mode 100644 index 000000000000..424178713503 --- /dev/null +++ b/scripts/ci-build.ts @@ -0,0 +1,42 @@ +import { execFileSync, ExecFileSyncOptions } from 'child_process' +import path from 'path' +import os from 'os' +import git from '@nice-labs/git-rev' + +const cwd = path.join(__dirname, '..') +const BUILD_PATH = path.join(cwd, 'build') +const stdio = [process.stdin, process.stdout, process.stderr] +const shell = os.platform() === 'win32' + +const exec = (command: string, args: string[], options?: ExecFileSyncOptions) => { + console.log('$', command, args.join(' '), '# cwd:', options?.cwd ?? cwd) + return execFileSync(command, args, { cwd, stdio, shell, ...options }) +} + +function buildTypes(name: string): string[] { + if (/full/.test(name) || name === 'master') { + return ['base', 'chromium', 'firefox', 'gecko', 'iOS'] + } else if (/ios/.test(name)) { + return ['iOS'] + } else if (/android|gecko/.test(name)) { + return ['firefox', 'gecko'] + } else { + return ['base', 'chromium', 'firefox'] + } +} + +const branch = git.branchName() +const types = buildTypes(branch.toLowerCase()) +console.log(`Branch: ${branch}`) +for (const type of types) { + console.log('#', 'Building for target:', type) + if (type === 'chromium' && types.includes('base')) { + // chromium doesn't have it's own changes yet. + // just copying base version is acceptable + exec('cp', ['-v', 'Maskbook.base.zip', 'Maskbook.chromium.zip']) + } else { + exec('yarn', [`build:${type.toLowerCase()}`]) + exec('zip', ['-FS', '-r', `../Maskbook.${type}.zip`, '.'], { cwd: BUILD_PATH }) + exec('rm', ['-rfv', 'build']) + } +} diff --git a/scripts/jest-global-teardown.js b/scripts/jest-global-teardown.js new file mode 100644 index 000000000000..0c0c42d5b58c --- /dev/null +++ b/scripts/jest-global-teardown.js @@ -0,0 +1 @@ +module.exports = () => {} diff --git a/scripts/jest-setup.js b/scripts/jest-setup.js index afd4f230aa9e..4b2f92d163ce 100644 --- a/scripts/jest-setup.js +++ b/scripts/jest-setup.js @@ -55,3 +55,8 @@ globalThis.webkit.messageHandlers = globalThis.webkit.messageHandlers || {} globalThis.webkit.messageHandlers.maskbookjsonrpc = { postMessage(data) {}, } + +// webpack env +globalThis.webpackEnv = { + target: 'Chromium', +} diff --git a/scripts/postinstall.js b/scripts/postinstall.js deleted file mode 100644 index 8f8cda55419b..000000000000 --- a/scripts/postinstall.js +++ /dev/null @@ -1,29 +0,0 @@ -const { spawn } = require('./spawn') -const path = require('path') - -const base = path.join(__dirname, '../') -process.chdir(base) -;(async () => { - if (process.argv.indexOf('--upgrade') !== -1) await spawn('yarn', ['upgrade', '@holoflows/kit']) - process.chdir('node_modules/@holoflows/kit') - await spawn('yarn', ['install']) - try { - /** - * For unknown reason, first time build will raise an exception. But if we build it twice, problem will be fixed - * - * src/Extension/AutomatedTabTask.ts:119:17 - - * error TS2742: The inferred type of 'AutomatedTabTask' cannot be named - * without a reference to '@holoflows/kit/node_modules/csstype'. - * This is likely not portable. - * A type annotation is necessary. - * 119 export function AutomatedTabTask PromiseLike>>( - * ~~~~~~~~~~~~~~~~ - */ - await spawn('yarn', ['build:tsc']) - await spawn('yarn', ['build:rollup']) - } catch (e) { - console.log('Build failed, retry one more time.') - await spawn('yarn', ['build:tsc']) - await spawn('yarn', ['build:rollup']) - } -})() diff --git a/scripts/postinstall.ts b/scripts/postinstall.ts new file mode 100644 index 000000000000..002d848209d0 --- /dev/null +++ b/scripts/postinstall.ts @@ -0,0 +1,12 @@ +import { execFileSync } from 'child_process' +import path from 'path' +import os from 'os' + +const cwd = path.join(__dirname, '..', 'node_modules', '@holoflows', 'kit') +const stdio = [process.stdin, process.stdout, process.stderr] +const shell = os.platform() === 'win32' +const yarn = (...args: string[]) => execFileSync('yarn', args, { cwd, stdio, shell }) + +yarn('install') +yarn('build:tsc') +yarn('build:rollup') diff --git a/scripts/spawn.js b/scripts/spawn.js deleted file mode 100644 index b2839e1e9cc7..000000000000 --- a/scripts/spawn.js +++ /dev/null @@ -1,39 +0,0 @@ -const { spawn } = require('child_process') - -process.on('unhandledRejection', e => { - process.exit(1) -}) -/** - * - * @param {string} command Command - * @param {string[]} args Arguments - * @param {import('child_process').SpawnOptionsWithoutStdio} options Options - */ -function spawnAsync(command, args, options = {}) { - const child = spawn(command, args, { stdio: 'inherit', shell: true, ...options }) - let stdout = '' - let stderr = '' - if (child.stdout) { - child.stdout.addListener('data', data => (stdout += data.toString())) - } - if (child.stderr) { - child.stderr.addListener('data', data => (stderr += data.toString())) - } - return new Promise(function(resolve, reject) { - child.addListener('error', e => { - child.kill() - reject(e) - }) - child.addListener('exit', code => { - if (code === 0) return resolve(stdout) - const error = new Error('Child exited with code: ' + code) - console.log(stdout) - console.error(stderr) - reject(error) - }) - }) -} - -module.exports = { - spawn: spawnAsync, -} diff --git a/src/background-service.ts b/src/background-service.ts index fd62e49510c7..dcc8e3fcf035 100644 --- a/src/background-service.ts +++ b/src/background-service.ts @@ -42,13 +42,19 @@ if (GetContext() === 'background') { browser.webNavigation.onCommitted.addListener(async arg => { if (arg.url === 'about:blank') return await contentScriptReady - browser.tabs - .executeScript(arg.tabId, { - runAt: 'document_start', - frameId: arg.frameId, - code: injectedScript, - }) - .catch(IgnoreError(arg)) + /** + * For WKWebview, there is a special way to do it in the manifest.json + * + * A `iOS-injected-scripts` field is used to add extra scripts + */ + if (webpackEnv.target !== 'WKWebview') + browser.tabs + .executeScript(arg.tabId, { + runAt: 'document_start', + frameId: arg.frameId, + code: injectedScript, + }) + .catch(IgnoreError(arg)) for (const script of contentScripts) { const option: browser.extensionTypes.InjectDetails = { runAt: 'document_idle', diff --git a/src/components/InjectedComponents/CommentBox.tsx b/src/components/InjectedComponents/CommentBox.tsx index ecea46936593..5bbd60c6995a 100644 --- a/src/components/InjectedComponents/CommentBox.tsx +++ b/src/components/InjectedComponents/CommentBox.tsx @@ -36,14 +36,14 @@ export interface CommentBoxProps { } export function CommentBox(props: CommentBoxProps) { const classes = useStyles() - const [binder, inputRef] = useCapturedInput(() => {}) + const [binder, inputRef, node] = useCapturedInput(() => {}) const { t } = useI18N() useEffect( binder(['keypress'], e => { - if (!inputRef.current) return + if (!node) return if (e.key === 'Enter') { - props.onSubmit(inputRef.current.value) - inputRef.current.value = '' + props.onSubmit(node.value) + node.value = '' } }), ) diff --git a/src/components/InjectedComponents/ImmersiveSetup/DraggablePaper.tsx b/src/components/InjectedComponents/ImmersiveSetup/DraggableDiv.tsx similarity index 71% rename from src/components/InjectedComponents/ImmersiveSetup/DraggablePaper.tsx rename to src/components/InjectedComponents/ImmersiveSetup/DraggableDiv.tsx index 5f32daba0cb7..ef098f58528e 100644 --- a/src/components/InjectedComponents/ImmersiveSetup/DraggablePaper.tsx +++ b/src/components/InjectedComponents/ImmersiveSetup/DraggableDiv.tsx @@ -1,5 +1,4 @@ import React from 'react' -import Paper, { PaperProps } from '@material-ui/core/Paper' import Draggable from 'react-draggable' import { makeStyles, Theme } from '@material-ui/core' @@ -19,15 +18,14 @@ const useStyle = makeStyles((theme: Theme) => ({ top: '2em', right: '2em', pointerEvents: 'initial', - outline: theme.palette.type === 'dark' ? '1px solid rgba(255, 255, 255, 0.5)' : undefined, }, })) -export function DraggablePaper(props: PaperProps) { +export function DraggableDiv(props: React.HTMLAttributes) { const classes = useStyle() return (
- +
) diff --git a/src/components/InjectedComponents/ImmersiveSetup/SetupStepper.tsx b/src/components/InjectedComponents/ImmersiveSetup/SetupStepper.tsx index 28c8e81460bb..efdd97f689e1 100644 --- a/src/components/InjectedComponents/ImmersiveSetup/SetupStepper.tsx +++ b/src/components/InjectedComponents/ImmersiveSetup/SetupStepper.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useState, useCallback } from 'react' import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' import Stepper from '@material-ui/core/Stepper' import Step from '@material-ui/core/Step' @@ -6,7 +6,7 @@ import StepLabel from '@material-ui/core/StepLabel' import StepContent from '@material-ui/core/StepContent' import Button from '@material-ui/core/Button' import Typography from '@material-ui/core/Typography' -import { TextField, Link, AppBar, Toolbar, IconButton } from '@material-ui/core' +import { TextField, AppBar, Toolbar, IconButton, Paper, Collapse } from '@material-ui/core' import { ActionButtonPromise } from '../../../extension/options-page/DashboardComponents/ActionButton' import { sleep } from '@holoflows/kit/es/util/sleep' import { getActivatedUI } from '../../../social-network/ui' @@ -14,10 +14,13 @@ import { useValueRef } from '../../../utils/hooks/useValueRef' import { ProfileIdentifier, PersonaIdentifier } from '../../../database/type' import { useCapturedInput } from '../../../utils/hooks/useCapturedEvents' import CloseIcon from '@material-ui/icons/Close' +import RemoveIcon from '@material-ui/icons/Remove' +import AddIcon from '@material-ui/icons/Add' import { currentImmersiveSetupStatus, ImmersiveSetupCrossContextStatus } from '../../shared-settings/settings' import Services from '../../../extension/service' import { useI18N } from '../../../utils/i18n-next-ui' import { selectElementContents } from '../../../utils/utils' +import classNames from 'classnames' const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -45,7 +48,14 @@ const useStyles = makeStyles((theme: Theme) => padding: 6, border: `1px solid ${theme.palette.error.main}`, }, - header: { cursor: 'move' }, + header: { cursor: 'move', transition: 'opacity 0.4s ease' }, + minimizedHeader: { + opacity: 0.26, + }, + body: { + transition: 'transform 0.1s ease', + transformOrigin: 'top center', + }, }), ) @@ -72,6 +82,11 @@ export function ImmersiveSetupStepperUI(props: ImmersiveSetupStepperUIProps) { const steps = getSteps() const activeStep = props.currentStep const [, inputRef] = useCapturedInput(props.onUsernameChange) + const [minimized, setMinimized] = useState( + () => + !!globalThis.location.href.match(/\/login(?:\.php|\/)/) || + getActivatedUI().lastRecognizedIdentity.value.identifier.isUnknown, + ) const ERROR_TEXT = t('immersive_setup_no_bio_got') @@ -88,25 +103,43 @@ export function ImmersiveSetupStepperUI(props: ImmersiveSetupStepperUIProps) { ) return ( ) diff --git a/src/components/InjectedComponents/PostDialog.tsx b/src/components/InjectedComponents/PostDialog.tsx index a606cb9d07ad..0a14d7a98db1 100644 --- a/src/components/InjectedComponents/PostDialog.tsx +++ b/src/components/InjectedComponents/PostDialog.tsx @@ -41,6 +41,7 @@ import { PluginRedPacketTheme } from '../../plugins/Wallet/theme' import { sleep } from '../../utils/utils' import { useI18N } from '../../utils/i18n-next-ui' import ShadowRootDialog from '../../utils/jss/ShadowRootDialog' +import { twitterUrl } from '../../social-network-provider/twitter.com/utils/url' const defaultTheme = {} @@ -317,12 +318,14 @@ export function PostDialog(props: PostDialogProps) { if (metadata.ok) { if (i18n.language.includes('zh')) { text = - '春節快樂,用 Maskbook 開啟 Twitter 上第一個紅包! (僅限 Twitter web 版)#MakerDAO #Maskbook 用@realMaskbook 解密 ' + - encrypted + activeUI.networkIdentifier === twitterUrl.hostIdentifier + ? `用 #Maskbook @realMaskbook 開啟紅包 ${encrypted}` + : `用 #Maskbook 開啟紅包 ${encrypted}` } else { text = - 'Happy Chinese New Year and use Maskbook to receive the first Twitter Red Packet. (Only available on Twitter for web) #MakerDAO #Maskbook Decrypt with @realMaskbook ' + - encrypted + activeUI.networkIdentifier === twitterUrl.hostIdentifier + ? `Claim this Red Packet with #Maskbook @realMaskbook ${encrypted}` + : `Claim this Red Packet with #Maskbook ${encrypted}` } } activeUI.taskPasteIntoPostBox(text, { diff --git a/src/components/shared/SelectRecipients/SelectRecipients.tsx b/src/components/shared/SelectRecipients/SelectRecipients.tsx index d3fa44861a0c..ca8a6fecf37a 100644 --- a/src/components/shared/SelectRecipients/SelectRecipients.tsx +++ b/src/components/shared/SelectRecipients/SelectRecipients.tsx @@ -46,9 +46,9 @@ export function SelectRecipientsUI( x => isProfile(x) && !x.identifier.equals(currentIdentity?.identifier) && x.linkedPersona?.fingerprint, ) as Profile[] const [open, setOpen] = useState(false) + const selectedProfiles = selected.filter(x => isProfile(x)) as Profile[] const selectedGroups = selected.filter(x => isGroup(x)) as Group[] - - const groupMembers = React.useMemo( + const selectedGroupMembers = React.useMemo( () => selectedGroups.flatMap(group => group.members).map(identifier => identifier.toText()), [selectedGroups], ) @@ -75,7 +75,10 @@ export function SelectRecipientsUI( x.identifier.toText()), + ]).size, }), avatar: , disabled: props.disabled || profileItems.length === 0, @@ -88,7 +91,7 @@ export function SelectRecipientsUI( open={open} items={profileItems} selected={profileItems.filter(x => selected.includes(x))} - disabledItems={profileItems.filter(x => groupMembers.includes(x.identifier.toText()))} + disabledItems={profileItems.filter(x => selectedGroupMembers.includes(x.identifier.toText()))} disabled={false} submitDisabled={false} onSubmit={() => setOpen(false)} diff --git a/src/crypto/__tests__/crypto-alpha-38-test.ts b/src/crypto/__tests__/crypto-alpha-38-test.ts index 86e4da723d40..ddaf829f5fd8 100644 --- a/src/crypto/__tests__/crypto-alpha-38-test.ts +++ b/src/crypto/__tests__/crypto-alpha-38-test.ts @@ -26,6 +26,10 @@ async function aesFromSeed(seed: string) { return derive_AES_GCM_256_Key_From_PBKDF2(await import_PBKDF2_Key(encodeText(seed)), encodeText('iv')) } +beforeAll(() => { + spyOn(globalThis.console, 'warn') +}) + // Test for: c.typedMessageParse && c.typedMessageStringify test('Crypto alpha v38 Typed Message', () => { diff --git a/src/database/Persona/Persona.db.ts b/src/database/Persona/Persona.db.ts index 0cef0cf84708..a9452fac3f57 100644 --- a/src/database/Persona/Persona.db.ts +++ b/src/database/Persona/Persona.db.ts @@ -37,6 +37,7 @@ const db = createDBAccess(() => { }, }) }) +export const createPersonaDBAccess = db export type FullPersonaDBTransaction = IDBPSafeTransaction< PersonaDB, ['personas', 'profiles'], @@ -186,7 +187,7 @@ export async function updatePersonaDB( ...old, ...nextRecord, linkedProfiles: nextLinkedProfiles, - updatedAt: new Date(), + updatedAt: nextRecord.updatedAt ?? new Date(), }) await t.objectStore('personas').put(next) MessageCenter.emit('personaUpdated', undefined) @@ -202,8 +203,8 @@ export async function createOrUpdatePersonaDB( return createPersonaDB( { ...record, - createdAt: new Date(), - updatedAt: new Date(), + createdAt: record.createdAt ?? new Date(), + updatedAt: record.updatedAt ?? new Date(), linkedProfiles: new IdentifierMap(new Map()), }, t, diff --git a/src/database/Persona/consistency.ts b/src/database/Persona/consistency.ts index be330207babf..12e675d82451 100644 --- a/src/database/Persona/consistency.ts +++ b/src/database/Persona/consistency.ts @@ -121,7 +121,7 @@ async function* checkProfileLink( const designatedPersona = restorePrototype(invalidLinkedPersona, ECKeyIdentifier.prototype) const persona = await t.objectStore('personas').get(designatedPersona.toText()) if (!persona) { - yield { type: Type.One_Way_Link_In_Profile, profile: profile, designatedPersona } + yield { type: Type.One_Way_Link_In_Profile, profile, designatedPersona } } } diff --git a/src/database/Persona/helpers.ts b/src/database/Persona/helpers.ts index 58ef4ca525d6..a2321dc1d803 100644 --- a/src/database/Persona/helpers.ts +++ b/src/database/Persona/helpers.ts @@ -95,7 +95,7 @@ export async function queryProfilesWithQuery(query?: Parameters[0]): Promise { const _ = await queryPersonasDB(query || (_ => true)) @@ -193,6 +193,7 @@ export async function createProfileWithPersona( profileID: ProfileIdentifier, data: LinkedProfileDetails, keys: { + nickname?: string publicKey: JsonWebKey privateKey?: JsonWebKey localKey?: CryptoKey @@ -205,6 +206,7 @@ export async function createProfileWithPersona( updatedAt: new Date(), identifier: ec_id, linkedProfiles: new IdentifierMap(new Map(), ProfileIdentifier), + nickname: keys.nickname, publicKey: keys.publicKey, privateKey: keys.privateKey, localKey: keys.localKey, diff --git a/src/database/__tests__/IdentifierMap.ts b/src/database/__tests__/IdentifierMap.ts index b81b485cc6c3..c45958827702 100644 --- a/src/database/__tests__/IdentifierMap.ts +++ b/src/database/__tests__/IdentifierMap.ts @@ -4,6 +4,11 @@ import { ProfileIdentifier, Identifier, PostIVIdentifier, PostIdentifier } from const a = new ProfileIdentifier('localhost', 'id') const aa = new ProfileIdentifier('localhost', 'id') const b = new ProfileIdentifier('localhost', 'id2') + +beforeAll(() => { + spyOn(globalThis.console, 'warn') +}) + test('Map', () => { const x = new IdentifierMap(new Map()) x.set(a, 1) diff --git a/src/database/__tests__/Persona/Persona.db.ts b/src/database/__tests__/Persona/Persona.db.ts new file mode 100644 index 000000000000..1bf14c55ebd0 --- /dev/null +++ b/src/database/__tests__/Persona/Persona.db.ts @@ -0,0 +1,280 @@ +import uuid from 'uuid/v4' +import { IdentifierMap } from '../../IdentifierMap' +import { ProfileIdentifier, ECKeyIdentifier } from '../../type' +import { + PersonaRecord, + ProfileRecord, + createPersonaDB, + queryPersonaDB, + FullPersonaDBTransaction, + createPersonaDBAccess, + queryPersonasDB, + deletePersonaDB, + updatePersonaDB, + LinkedProfileDetails, + createProfileDB, + queryProfileDB, + queryProfilesDB, + updateProfileDB, + createOrUpdateProfileDB, + deleteProfileDB, + attachProfileDB, + queryPersonaByProfileDB, + queryPersonasWithPrivateKey, + createOrUpdatePersonaDB, + safeDeletePersonaDB, +} from '../../Persona/Persona.db' +import { generate_ECDH_256k1_KeyPair_ByMnemonicWord } from '../../../utils/mnemonic-code' +import { CryptoKeyToJsonWebKey } from '../../../utils/type-transform/CryptoKey-JsonWebKey' +import { deriveLocalKeyFromECDHKey } from '../../../utils/mnemonic-code/localKeyGenerate' +import { createTransaction } from '../../helpers/openDB' + +export async function createPersonaRecord(name: string = uuid(), password: string = uuid()) { + const key = await generate_ECDH_256k1_KeyPair_ByMnemonicWord(password) + const jwkPub = await CryptoKeyToJsonWebKey(key.key.publicKey) + const jwkPriv = await CryptoKeyToJsonWebKey(key.key.privateKey) + const localKey = await deriveLocalKeyFromECDHKey(key.key.publicKey, key.mnemonicRecord.words) + const jwkLocalKey = await CryptoKeyToJsonWebKey(localKey) + const identifier = ECKeyIdentifier.fromJsonWebKey(jwkPub) + + return { + nickname: name, + createdAt: new Date(), + updatedAt: new Date(), + identifier: identifier, + linkedProfiles: new IdentifierMap(new Map(), ProfileIdentifier), + publicKey: jwkPub, + privateKey: jwkPriv, + mnemonic: key.mnemonicRecord, + localKey: jwkLocalKey, + } as PersonaRecord +} + +export async function createProfileRecord( + identifier: ProfileIdentifier = new ProfileIdentifier(uuid(), uuid()), + name: string = uuid(), + password: string = uuid(), +) { + const { localKey, identifier: linkedPersona, createdAt, updatedAt } = await createPersonaRecord(name, password) + + return { + identifier, + nickname: name, + localKey, + linkedPersona, + createdAt, + updatedAt, + } as ProfileRecord +} + +export async function personaDBWriteAccess(action: (t: FullPersonaDBTransaction<'readwrite'>) => Promise) { + await action(createTransaction(await createPersonaDBAccess(), 'readwrite')('profiles', 'personas')) +} + +beforeAll(() => { + // MessageCenter will dispatch events on each tab + // but the mocking query method returns undefined + browser.tabs.query = async () => { + return [] + } +}) + +afterEach(async () => { + await personaDBWriteAccess(async t => { + await t.objectStore('personas').clear() + await t.objectStore('profiles').clear() + }) +}) + +test('createPersonaDB & queryPersonaDB', async () => { + const personaRecord = await createPersonaRecord() + await personaDBWriteAccess(t => createPersonaDB(personaRecord, t)) + expect(await queryPersonaDB(personaRecord.identifier)).toEqual(personaRecord) +}) + +test('queryPersonasDB', async () => { + const personaRecordA = await createPersonaRecord() + const personaRecordB = await createPersonaRecord() + const names = [personaRecordA.nickname, personaRecordB.nickname] + await personaDBWriteAccess(async t => { + await createPersonaDB(personaRecordA, t) + await createPersonaDB(personaRecordB, t) + }) + + const personaRecords = await queryPersonasDB(p => !!(p.nickname && names.includes(p.nickname))) + expect(personaRecords.length).toBe(2) + expect(personaRecords.every(r => names.includes(r.nickname ?? ''))).toBeTruthy() +}) + +test('updatePersonaDB - replace linked profiles', async () => { + const id = uuid() + const personaRecord = await createPersonaRecord(id, id) + const personaRecordNew = await createPersonaRecord(id, id) + personaRecordNew.identifier = personaRecord.identifier + + await personaDBWriteAccess(async t => { + await createPersonaDB(personaRecord, t) + await updatePersonaDB( + personaRecordNew, + { + linkedProfiles: 'replace', + explicitUndefinedField: 'ignore', + }, + t, + ) + }) + expect(await queryPersonaDB(personaRecord.identifier)).toEqual(personaRecordNew) +}) + +test('deletePersonaDB', async () => { + const personaRecord = await createPersonaRecord() + await personaDBWriteAccess(t => createPersonaDB(personaRecord, t)) + expect(await queryPersonaDB(personaRecord.identifier)).toEqual(personaRecord) + + await personaDBWriteAccess(t => deletePersonaDB(personaRecord.identifier, 'delete even with private', t)) + expect(await queryPersonaDB(personaRecord.identifier)).toEqual(null) +}) + +test('queryPersonaByProfileDB', async () => { + const profileRecord = await createProfileRecord() + const personaRecord = await createPersonaRecord() + profileRecord.linkedPersona = personaRecord.identifier + await personaDBWriteAccess(async t => { + await createProfileDB(profileRecord, t) + await createPersonaDB(personaRecord, t) + }) + + expect(await queryPersonaByProfileDB(profileRecord.identifier)).toEqual(personaRecord) +}) + +test('queryPersonasWithPrivateKey', async () => { + const personaRecordA = await createPersonaRecord() + const personaRecordB = await createPersonaRecord() + await personaDBWriteAccess(async t => { + await createPersonaDB(personaRecordA, t) + await createPersonaDB(personaRecordB, t) + }) + const names = (await queryPersonasWithPrivateKey()).map(p => p.nickname) + + expect(names.includes(personaRecordA.nickname)).toBe(true) + expect(names.includes(personaRecordB.nickname)).toBe(true) +}) + +test('createOrUpdatePersonaDB', async () => { + const personaRecord = await createPersonaRecord() + const personaRecordNew = await createPersonaRecord() + const howToMerge = { + linkedProfiles: 'replace', + explicitUndefinedField: 'ignore', + } as Parameters[1] + personaRecordNew.identifier = personaRecord.identifier + + expect(await queryPersonaDB(personaRecord.identifier)).toEqual(null) + + await personaDBWriteAccess(t => createOrUpdatePersonaDB(personaRecord, howToMerge, t)) + expect(await queryPersonaDB(personaRecord.identifier)).toEqual(personaRecord) + + await personaDBWriteAccess(t => createOrUpdatePersonaDB(personaRecordNew, howToMerge, t)) + expect(await queryPersonaDB(personaRecord.identifier)).toEqual(personaRecordNew) +}) + +test('safeDeletePersonaDB', async () => { + const personaRecord = await createPersonaRecord() + const howToMerge = { + linkedProfiles: 'replace', + explicitUndefinedField: 'delete field', + } as Parameters[1] + + await personaDBWriteAccess(t => createPersonaDB(personaRecord, t)) + expect(await queryPersonaDB(personaRecord.identifier)).toEqual(personaRecord) + + personaRecord.linkedProfiles.clear() + await personaDBWriteAccess(async t => { + await updatePersonaDB(personaRecord, howToMerge, t) + await safeDeletePersonaDB(personaRecord.identifier, t) + }) + expect(await queryPersonaDB(personaRecord.identifier)).toEqual(personaRecord) + + personaRecord.privateKey = undefined + await personaDBWriteAccess(async t => { + await updatePersonaDB(personaRecord, howToMerge, t) + await safeDeletePersonaDB(personaRecord.identifier, t) + }) + expect(await queryPersonaDB(personaRecord.identifier)).toEqual(null) +}) + +test('createProfileDB && queryProfileDB', async () => { + const profileRecord = await createProfileRecord() + await personaDBWriteAccess(t => createProfileDB(profileRecord, t)) + expect(await queryProfileDB(profileRecord.identifier)).toEqual(profileRecord) +}) + +test('queryProfilesDB', async () => { + const profileRecordA = await createProfileRecord() + const profileRecordB = await createProfileRecord() + const nicknames = [profileRecordA.nickname, profileRecordB.nickname] + const networkIds = [profileRecordA.identifier.network, profileRecordB.identifier.network] + await personaDBWriteAccess(async t => { + await createProfileDB(profileRecordA, t) + await createProfileDB(profileRecordB, t) + }) + + const profileRecords = await queryProfilesDB(p => networkIds.includes(p.identifier.network)) + expect(profileRecords.length).toBe(2) + expect(profileRecords.every(r => nicknames.includes(r.nickname ?? ''))).toBeTruthy() +}) + +test('updateProfileDB', async () => { + const profileRecord = await createProfileRecord() + const profileRecordNew = await createProfileRecord() + profileRecordNew.identifier = profileRecord.identifier + + await personaDBWriteAccess(async t => { + await createProfileDB(profileRecord, t) + await updateProfileDB(profileRecordNew, t) + }) + + expect(await queryProfileDB(profileRecord.identifier)).toEqual(profileRecordNew) +}) + +test('createOrUpdateProfileDB', async () => { + const profileRecord = await createProfileRecord() + const profileRecordNew = await createProfileRecord() + profileRecordNew.identifier = profileRecord.identifier + + expect(await queryProfileDB(profileRecord.identifier)).toEqual(null) + + await personaDBWriteAccess(t => createOrUpdateProfileDB(profileRecord, t)) + expect(await queryProfileDB(profileRecord.identifier)).toEqual(profileRecord) + + await personaDBWriteAccess(t => createOrUpdateProfileDB(profileRecordNew, t)) + expect(await queryProfileDB(profileRecord.identifier)).toEqual(profileRecordNew) +}) + +test('attachProfileDB && detachProfileDB', async () => { + const profileRecord = await createProfileRecord() + const personaRecord = await createPersonaRecord() + + await personaDBWriteAccess(async t => { + await createPersonaDB(personaRecord, t) + await attachProfileDB( + profileRecord.identifier, + personaRecord.identifier, + { + connectionConfirmState: 'confirmed', + }, + t, + ) + }) + expect((await queryProfileDB(profileRecord.identifier))?.linkedPersona).toEqual(personaRecord.identifier) +}) + +test('deleteProfileDB', async () => { + const profileRecord = await createProfileRecord() + + await personaDBWriteAccess(t => createProfileDB(profileRecord, t)) + expect(await queryProfileDB(profileRecord.identifier)).toEqual(profileRecord) + + await personaDBWriteAccess(t => deleteProfileDB(profileRecord.identifier, t)) + expect(await queryProfileDB(profileRecord.identifier)).toEqual(null) +}) diff --git a/src/database/__tests__/Persona/helpers.ts b/src/database/__tests__/Persona/helpers.ts new file mode 100644 index 000000000000..ff5aba25f3ae --- /dev/null +++ b/src/database/__tests__/Persona/helpers.ts @@ -0,0 +1,233 @@ +import uuid from 'uuid/v4' +import { web3 } from '../../../plugins/Wallet/web3' +import { + createPersonaByMnemonic, + queryPersona, + profileRecordToProfile, + personaRecordToPersona, + queryProfile, + queryProfilesWithQuery, + queryPersonasWithQuery, + renamePersona, + queryPersonaByProfile, + queryPersonaRecord, + queryPublicKey, + queryLocalKey, + queryPrivateKey, + createProfileWithPersona, + deletePersona, +} from '../../Persona/helpers' +import { queryPersonaDB, createProfileDB, createPersonaDB, queryProfileDB } from '../../Persona/Persona.db' +import { createPersonaRecord, createProfileRecord, personaDBWriteAccess } from './Persona.db' +import { storeAvatarDB } from '../../avatar' +import { JsonWebKeyToCryptoKey, getKeyParameter } from '../../../utils/type-transform/CryptoKey-JsonWebKey' + +beforeAll(() => { + // MessageCenter will dispatch events on each tab + // but default query method returns undefined + browser.tabs.query = async () => { + return [] + } +}) + +afterEach(async () => { + await personaDBWriteAccess(async t => { + await t.objectStore('personas').clear() + await t.objectStore('profiles').clear() + }) +}) + +test('profileRecordToProfile', async () => { + const profileRecord = await createProfileRecord() + await storeAvatarDB(profileRecord.identifier, new ArrayBuffer(20)) + + const profile = await profileRecordToProfile(profileRecord) + expect(profile.avatar).toBe('data:image/png;base64,AAAAAAAAAAAAAAAAAAAAAAAAAAA=') + expect(profile.linkedPersona?.identifier).toEqual(profileRecord.linkedPersona) +}) + +test('personaRecordToPersona', async () => { + const personaRecord = await createPersonaRecord() + const persona = personaRecordToPersona(personaRecord) + + expect(persona.hasPrivateKey).toBe(true) + expect(persona.fingerprint).toBe(personaRecord.identifier.compressedPoint) +}) + +test('queryProfile', async () => { + const profileRecord = await createProfileRecord() + const fake = await queryProfile(profileRecord.identifier) + expect(fake.avatar).toBe(undefined) + expect(fake.linkedPersona).toBe(undefined) + + await storeAvatarDB(profileRecord.identifier, new ArrayBuffer(20)) + await personaDBWriteAccess(t => createProfileDB(profileRecord, t)) + + const real = await queryProfile(profileRecord.identifier) + expect(real.avatar).toBe('data:image/png;base64,AAAAAAAAAAAAAAAAAAAAAAAAAAA=') + expect(real.linkedPersona?.identifier).toEqual(profileRecord.linkedPersona) +}) + +test('queryPersona', async () => { + const personaRecord = await createPersonaRecord() + const fake = await queryPersona(personaRecord.identifier) + expect(fake.hasPrivateKey).toBe(false) + expect(fake.fingerprint).toBe(personaRecord.identifier.compressedPoint) + + await personaDBWriteAccess(t => createPersonaDB(personaRecord, t)) + + const real = await queryPersona(personaRecord.identifier) + expect(real.hasPrivateKey).toBe(true) + expect(real.fingerprint).toBe(personaRecord.identifier.compressedPoint) +}) + +test('queryProfilesWithQuery', async () => { + const profileRecordA = await createProfileRecord() + const profileRecordB = await createProfileRecord() + const names = [profileRecordA.nickname, profileRecordB.nickname] + + await personaDBWriteAccess(async t => { + await createProfileDB(profileRecordA, t) + await createProfileDB(profileRecordB, t) + }) + + const profiles = await queryProfilesWithQuery(({ nickname }) => names.includes(nickname)) + expect(profiles.every(p => names.includes(p.nickname))).toBe(true) +}) + +test('queryPersonasWithQuery', async () => { + const personaRecordA = await createPersonaRecord() + const personaRecordB = await createPersonaRecord() + const names = [personaRecordA.nickname, personaRecordB.nickname] + + await personaDBWriteAccess(async t => { + await createPersonaDB(personaRecordA, t) + await createPersonaDB(personaRecordB, t) + }) + + const personas = await queryPersonasWithQuery(({ nickname }) => names.includes(nickname)) + expect(personas.every(p => names.includes(p.nickname))).toBe(true) +}) + +test('deletePersona', async () => { + const personaRecord = await createPersonaRecord() + await personaDBWriteAccess(t => createPersonaDB(personaRecord, t)) + expect(await queryPersonaDB(personaRecord.identifier)).toEqual(personaRecord) + + await deletePersona(personaRecord.identifier, 'delete even with private') + expect(await queryPersonaDB(personaRecord.identifier)).toEqual(null) +}) + +test('renamePersona', async () => { + const name = uuid() + const personaRecord = await createPersonaRecord() + + await personaDBWriteAccess(t => createPersonaDB(personaRecord, t)) + await renamePersona(personaRecord.identifier, name) + expect((await queryPersonaDB(personaRecord.identifier))?.nickname).toBe(name) +}) + +test('queryPersonaByProfile', async () => { + const profileRecord = await createProfileRecord() + const personaRecord = await createPersonaRecord() + profileRecord.linkedPersona = personaRecord.identifier + personaRecord.linkedProfiles.set(profileRecord.identifier, { + connectionConfirmState: 'confirmed', + }) + await personaDBWriteAccess(async t => { + await createProfileDB(profileRecord, t) + await createPersonaDB(personaRecord, t) + }) + expect(await queryPersonaByProfile(profileRecord.identifier)).toEqual(personaRecordToPersona(personaRecord)) +}) + +test('queryPersonaRecord', async () => { + const profileRecord = await createProfileRecord() + const personaRecord = await createPersonaRecord() + profileRecord.linkedPersona = personaRecord.identifier + personaRecord.linkedProfiles.set(profileRecord.identifier, { + connectionConfirmState: 'confirmed', + }) + await personaDBWriteAccess(async t => { + await createProfileDB(profileRecord, t) + await createPersonaDB(personaRecord, t) + }) + expect(await queryPersonaRecord(profileRecord.identifier)).toEqual(personaRecord) + expect(await queryPersonaRecord(personaRecord.identifier)).toEqual(personaRecord) +}) + +test('queryPublicKey', async () => { + const profileRecord = await createProfileRecord() + const personaRecord = await createPersonaRecord() + const publicKey = await JsonWebKeyToCryptoKey(personaRecord.publicKey, ...getKeyParameter('ecdh')) + profileRecord.linkedPersona = personaRecord.identifier + personaRecord.linkedProfiles.set(profileRecord.identifier, { + connectionConfirmState: 'confirmed', + }) + await personaDBWriteAccess(async t => { + await createProfileDB(profileRecord, t) + await createPersonaDB(personaRecord, t) + }) + expect(await queryPublicKey(profileRecord.identifier)).toEqual(publicKey) + expect(await queryPublicKey(personaRecord.identifier)).toEqual(publicKey) +}) + +test('queryPrivateKey', async () => { + const profileRecord = await createProfileRecord() + const personaRecord = await createPersonaRecord() + const privateKey = await JsonWebKeyToCryptoKey(personaRecord.privateKey!, ...getKeyParameter('ecdh')) + profileRecord.linkedPersona = personaRecord.identifier + personaRecord.linkedProfiles.set(profileRecord.identifier, { + connectionConfirmState: 'confirmed', + }) + await personaDBWriteAccess(async t => { + await createProfileDB(profileRecord, t) + await createPersonaDB(personaRecord, t) + }) + expect(await queryPrivateKey(profileRecord.identifier)).toEqual(privateKey) + expect(await queryPrivateKey(personaRecord.identifier)).toEqual(privateKey) +}) + +test('createPersonaByMnemonic & createPersonaByJsonWebKey', async () => { + // getBalance will be called in the event chain reaction + web3.eth.getBalance = async () => '0' + + const identifier = await createPersonaByMnemonic('test', 'test') + const persona = await queryPersonaDB(identifier) + expect(persona?.identifier).toEqual(identifier) + expect(persona?.nickname).toEqual('test') + expect(persona?.mnemonic?.parameter.withPassword).toEqual(true) +}) + +test('createProfileWithPersona', async () => { + const profileRecord = await createProfileRecord() + const personaRecord = await createPersonaRecord() + await createProfileWithPersona( + profileRecord.identifier, + { + connectionConfirmState: 'confirmed', + }, + personaRecord, + ) + profileRecord.linkedPersona = personaRecord.identifier + personaRecord.linkedProfiles.set(profileRecord.identifier, { + connectionConfirmState: 'confirmed', + }) + expect((await queryProfileDB(profileRecord.identifier))?.linkedPersona).toEqual(personaRecord.identifier) + expect((await queryPersonaDB(personaRecord.identifier))?.identifier).toEqual(personaRecord.identifier) +}) + +test('queryLocalKey', async () => { + const profileRecord = await createProfileRecord() + const personaRecord = await createPersonaRecord() + profileRecord.linkedPersona = personaRecord.identifier + personaRecord.linkedProfiles.set(profileRecord.identifier, { + connectionConfirmState: 'confirmed', + }) + await personaDBWriteAccess(async t => { + await createProfileDB(profileRecord, t) + await createPersonaDB(personaRecord, t) + }) + expect(await queryLocalKey(profileRecord.identifier)).toEqual(profileRecord.localKey) + expect(await queryLocalKey(personaRecord.identifier)).toEqual(personaRecord.localKey) +}) diff --git a/src/database/__tests__/avatar-db.ts b/src/database/__tests__/avatar-db.ts deleted file mode 100644 index 35bd66a4326c..000000000000 --- a/src/database/__tests__/avatar-db.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as a from '../avatar' -import { ProfileIdentifier } from '../type' - -const testID = new ProfileIdentifier('localhost', 'test') -test('Store & Query avatar', async () => { - await a.storeAvatarDB(testID, new ArrayBuffer(20)) - const result = await a.queryAvatarDB(testID) - expect(result?.byteLength).toBe(20) -}) diff --git a/src/database/__tests__/avatar.ts b/src/database/__tests__/avatar.ts new file mode 100644 index 000000000000..e2739f3b1d3a --- /dev/null +++ b/src/database/__tests__/avatar.ts @@ -0,0 +1,85 @@ +import uuid from 'uuid/v4' +import { + storeAvatarDB, + queryAvatarDB, + updateAvatarMetaDB, + queryAvatarOutdatedDB, + isAvatarOutdatedDB, + deleteAvatarsDB, +} from '../avatar' +import { ProfileIdentifier, GroupIdentifier } from '../type' + +function createBeforeDate(beforeDays: number) { + return new Date(Date.now() - 1000 * 60 * 60 * 24 * beforeDays) +} + +function createProfileIdentifier() { + return new ProfileIdentifier(uuid(), uuid()) +} + +function createGroupIdentifier() { + return new GroupIdentifier(uuid(), uuid(), uuid()) +} + +function createArrayBuffer(length: number) { + return new Uint8Array(new Array(length).fill(0).map(() => Math.round(Math.random() * 256))).buffer +} + +test('storeAvatarDB & queryAvatarDB', async () => { + const ab = createArrayBuffer(20) + const profileIdentifier = createProfileIdentifier() + const groupIdentifier = createGroupIdentifier() + + await storeAvatarDB(profileIdentifier, ab) + expect(await queryAvatarDB(profileIdentifier)).toEqual(ab) + + await storeAvatarDB(groupIdentifier, ab) + expect(await queryAvatarDB(groupIdentifier)).toEqual(ab) +}) + +test('updateAvatarMetaDB & queryAvatarOutdatedDB', async () => { + const ab = createArrayBuffer(20) + const profileIdentifierA = createProfileIdentifier() + const profileIdentifierB = createProfileIdentifier() + await storeAvatarDB(profileIdentifierA, ab) + await storeAvatarDB(profileIdentifierB, ab) + await updateAvatarMetaDB(profileIdentifierA, { + lastAccessTime: createBeforeDate(15), + }) + await updateAvatarMetaDB(profileIdentifierB, { + lastUpdateTime: createBeforeDate(8), + }) + expect(await queryAvatarOutdatedDB('lastAccessTime')).toEqual([profileIdentifierA]) + expect(await queryAvatarOutdatedDB('lastUpdateTime')).toEqual([profileIdentifierB]) +}) + +test('updateAvatarMetaDB & isAvatarOutdatedDB', async () => { + const ab = createArrayBuffer(20) + const profileIdentifier = createProfileIdentifier() + await storeAvatarDB(profileIdentifier, ab) + expect(await isAvatarOutdatedDB(profileIdentifier, 'lastAccessTime')).toBeFalsy() + expect(await isAvatarOutdatedDB(profileIdentifier, 'lastUpdateTime')).toBeFalsy() + + await updateAvatarMetaDB(profileIdentifier, { + lastAccessTime: createBeforeDate(31), + }) + expect(await isAvatarOutdatedDB(profileIdentifier, 'lastAccessTime')).toBeTruthy() + + await updateAvatarMetaDB(profileIdentifier, { + lastUpdateTime: createBeforeDate(8), + }) + expect(await isAvatarOutdatedDB(profileIdentifier, 'lastUpdateTime')).toBeTruthy() +}) + +test('deleteAvatarsDB', async () => { + const ab = createArrayBuffer(20) + const profileIdentifier = createProfileIdentifier() + + expect(await queryAvatarDB(profileIdentifier)).toBe(null) + + await storeAvatarDB(profileIdentifier, ab) + expect(await queryAvatarDB(profileIdentifier)).toEqual(ab) + + await deleteAvatarsDB([profileIdentifier]) + expect(await queryAvatarDB(profileIdentifier)).toEqual(null) +}) diff --git a/src/database/__tests__/group.ts b/src/database/__tests__/group.ts new file mode 100644 index 000000000000..5dfa3c876f9c --- /dev/null +++ b/src/database/__tests__/group.ts @@ -0,0 +1,86 @@ +import uuid from 'uuid/v4' +import { + GroupRecord, + createUserGroupDatabase, + queryUserGroupDatabase, + queryUserGroupsDatabase, + createOrUpdateUserGroupDatabase, + deleteUserGroupDatabase, + updateUserGroupDatabase, +} from '../group' +import { GroupIdentifier } from '../type' + +function createGroupRecord() { + const identifier = new GroupIdentifier(uuid(), uuid(), uuid()) + return { + groupName: uuid(), + identifier, + members: [], + banned: undefined, + } as GroupRecord +} + +function groupRecordToDB(groupRecord: GroupRecord) { + return { + ...groupRecord, + network: groupRecord.identifier.network, + } +} + +test('createUserGroupDatabase & queryUserGroupDatabase', async () => { + const groupRecord = createGroupRecord() + await createUserGroupDatabase(groupRecord.identifier, groupRecord.groupName) + expect(await queryUserGroupDatabase(groupRecord.identifier)).toEqual(groupRecordToDB(groupRecord)) +}) + +test('createOrUpdateUserGroupDatabase', async () => { + const groupRecord = createGroupRecord() + const groupRecordNew = createGroupRecord() + groupRecordNew.identifier = groupRecord.identifier + + expect(await queryUserGroupDatabase(groupRecord.identifier)).toBe(null) + + await createOrUpdateUserGroupDatabase(groupRecord, 'replace') + expect(await queryUserGroupDatabase(groupRecord.identifier)).toEqual(groupRecordToDB(groupRecord)) + + await createOrUpdateUserGroupDatabase(groupRecordNew, 'replace') + expect(await queryUserGroupDatabase(groupRecord.identifier)).toEqual(groupRecordToDB(groupRecordNew)) +}) + +test('deleteUserGroupDatabase', async () => { + const groupRecord = createGroupRecord() + expect(await queryUserGroupDatabase(groupRecord.identifier)).toBe(null) + + await createUserGroupDatabase(groupRecord.identifier, groupRecord.groupName) + expect(await queryUserGroupDatabase(groupRecord.identifier)).toEqual(groupRecordToDB(groupRecord)) + + await deleteUserGroupDatabase(groupRecord.identifier) + expect(await queryUserGroupDatabase(groupRecord.identifier)).toBe(null) +}) + +test('updateUserGroupDatabase', async () => { + const groupRecord = createGroupRecord() + const groupRecordNew = createGroupRecord() + groupRecordNew.identifier = groupRecord.identifier + + await createUserGroupDatabase(groupRecord.identifier, groupRecord.groupName) + expect(await queryUserGroupDatabase(groupRecord.identifier)).toEqual(groupRecordToDB(groupRecord)) + + await updateUserGroupDatabase(groupRecordNew, 'replace') + expect(await queryUserGroupDatabase(groupRecord.identifier)).toEqual(groupRecordToDB(groupRecordNew)) +}) + +test('queryUserGroupsDatabase', async () => { + const groupRecordA = createGroupRecord() + const groupRecordB = createGroupRecord() + const networks = [groupRecordA.identifier.network, groupRecordB.identifier.network] + await createUserGroupDatabase(groupRecordA.identifier, groupRecordA.groupName) + await createUserGroupDatabase(groupRecordB.identifier, groupRecordB.groupName) + expect(await queryUserGroupsDatabase({ network: groupRecordA.identifier.network })).toEqual([ + groupRecordToDB(groupRecordA), + ]) + expect(await queryUserGroupsDatabase({ network: groupRecordB.identifier.network })).toEqual([ + groupRecordToDB(groupRecordB), + ]) + expect(await queryUserGroupsDatabase(key => networks.includes(key.network))).toBeTruthy() +}) diff --git a/src/database/__tests__/helpers/group.ts b/src/database/__tests__/helpers/group.ts new file mode 100644 index 000000000000..5ea93b99d34b --- /dev/null +++ b/src/database/__tests__/helpers/group.ts @@ -0,0 +1,57 @@ +import uuid from 'uuid/v4' +import { ProfileIdentifier, GroupIdentifier, PreDefinedVirtualGroupNames } from '../../type' +import { + createFriendsGroup, + queryUserGroups, + addProfileToFriendsGroup, + removeProfileFromFriendsGroup, +} from '../../helpers/group' +import { queryUserGroupDatabase } from '../../group' + +function createProfileIdentifier(network = uuid(), userId = uuid()) { + return new ProfileIdentifier(network, userId) +} + +function createGroupIdentifier(network = uuid(), owner = uuid(), groupId = uuid()) { + return new GroupIdentifier(network, owner, groupId) +} + +test('createFriendsGroup', async () => { + const groupId = uuid() + const profileIdentifier = createProfileIdentifier() + const groupIdentifier = createGroupIdentifier(profileIdentifier.network, profileIdentifier.userId, groupId) + await createFriendsGroup(profileIdentifier, groupId) + expect(await queryUserGroupDatabase(groupIdentifier)).toBeTruthy() +}) + +test('createDefaultFriendsGroup', async () => { + const groupId = PreDefinedVirtualGroupNames.friends + const profileIdentifier = createProfileIdentifier() + const groupIdentifier = createGroupIdentifier(profileIdentifier.network, profileIdentifier.userId, groupId) + await createFriendsGroup(profileIdentifier, groupId) + expect(await queryUserGroupDatabase(groupIdentifier)).toBeTruthy() +}) + +test('addProfileToFriendsGroup & removeProfileFromFriendsGroup', async () => { + const groupId = uuid() + const ownerIdentifier = createProfileIdentifier() + const profileIdentifierA = createProfileIdentifier() + const profileIdentifierB = createProfileIdentifier() + const groupIdentifier = createGroupIdentifier(ownerIdentifier.network, ownerIdentifier.userId, groupId) + + await createFriendsGroup(ownerIdentifier, groupId) + expect((await queryUserGroupDatabase(groupIdentifier))?.members.length).toBe(0) + + await addProfileToFriendsGroup(groupIdentifier, [profileIdentifierA, profileIdentifierB]) + expect((await queryUserGroupDatabase(groupIdentifier))?.members.length).toBe(2) + + await removeProfileFromFriendsGroup(groupIdentifier, [profileIdentifierA, profileIdentifierB]) + expect((await queryUserGroupDatabase(groupIdentifier))?.members.length).toBe(0) +}) + +test('queryUserGroups', async () => { + const groupId = uuid() + const profileIdentifier = createProfileIdentifier() + await createFriendsGroup(profileIdentifier, groupId) + expect((await queryUserGroups(profileIdentifier.network)).length).toBe(1) +}) diff --git a/src/database/__tests__/post.ts b/src/database/__tests__/post.ts new file mode 100644 index 000000000000..c678bac34f25 --- /dev/null +++ b/src/database/__tests__/post.ts @@ -0,0 +1,100 @@ +import uuid from 'uuid/v4' +import { + PostRecord, + createPostDB, + queryPostDB, + updatePostDB, + createOrUpdatePostDB, + queryPostsDB, + deletePostCryptoKeyDB, + recipientsToNext, + recipientsFromNext, +} from '../post' +import { ProfileIdentifier, PostIVIdentifier, GroupIdentifier } from '../type' +import { generate_AES_GCM_256_Key } from '../../utils/crypto.subtle' + +async function createPostRecord( + postBy: ProfileIdentifier = new ProfileIdentifier(uuid(), uuid()), + identifier: PostIVIdentifier = new PostIVIdentifier(uuid(), uuid()), +) { + return { + postBy, + identifier, + postCryptoKey: await generate_AES_GCM_256_Key(), + recipients: {}, + recipientGroups: [], + foundAt: new Date(), + } as PostRecord +} + +test('createPostDB & queryPostDB', async () => { + const postRecord = await createPostRecord() + await createPostDB(postRecord) + expect(await queryPostDB(postRecord.identifier)).toEqual(postRecord) +}) + +test('updatePostDB', async () => { + const postRecord = await createPostRecord() + const postRecordNew = await createPostRecord() + postRecordNew.identifier = postRecord.identifier + await createPostDB(postRecord) + await updatePostDB(postRecordNew, 'append') + expect(await queryPostDB(postRecord.identifier)).toEqual(postRecordNew) +}) + +test('createOrUpdatePostDB', async () => { + const postRecord = await createPostRecord() + const postRecordNew = await createPostRecord() + postRecordNew.identifier = postRecord.identifier + + expect(await queryPostDB(postRecord.identifier)).toBe(null) + + await createOrUpdatePostDB(postRecord, 'append') + expect(await queryPostDB(postRecord.identifier)).toEqual(postRecord) + + await createOrUpdatePostDB(postRecordNew, 'append') + expect(await queryPostDB(postRecord.identifier)).toEqual(postRecordNew) +}) + +test('queryPostsDB', async () => { + const postRecordA = await createPostRecord() + const postRecordB = await createPostRecord() + const networks = [postRecordA.identifier.network, postRecordB.identifier.network] + const predicate = ({ identifier }: PostRecord) => networks.includes(identifier.network) + + await createPostDB(postRecordA) + await createPostDB(postRecordB) + expect(await queryPostsDB(postRecordA.identifier.network)).toEqual([postRecordA]) + expect((await queryPostsDB(predicate)).every(predicate)).toBeTruthy() +}) + +test('deletePostCryptoKeyDB', async () => { + const postRecord = await createPostRecord() + await createPostDB(postRecord) + expect(await queryPostDB(postRecord.identifier)).toEqual(postRecord) + + await deletePostCryptoKeyDB(postRecord.identifier) + expect(await queryPostDB(postRecord.identifier)).toEqual(null) +}) + +test('recipientsToNext & recipientsFromNext', async () => { + const profileIdentifierA = new ProfileIdentifier(uuid(), uuid()) + const profileIdentifierB = new ProfileIdentifier(uuid(), uuid()) + const groupIdentifier = new GroupIdentifier(uuid(), uuid(), uuid()) + const recipients = { + [profileIdentifierA.toText()]: { + reason: [{ type: 'direct', at: new Date() }], + }, + [profileIdentifierB.toText()]: { + reason: [{ type: 'group', group: groupIdentifier, at: new Date() }], + }, + } as PostRecord['recipients'] + + const next = recipientsToNext(recipients) + expect(next.get(profileIdentifierA)?.reason).toEqual(new Set(recipients[profileIdentifierA.toText()].reason)) + expect(next.get(profileIdentifierB)?.reason).toEqual(new Set(recipients[profileIdentifierB.toText()].reason)) + + const previous = recipientsFromNext(next) + expect(new Set(previous[profileIdentifierA.toText()].reason)).toEqual(next.get(profileIdentifierA)?.reason) + expect(new Set(previous[profileIdentifierB.toText()].reason)).toEqual(next.get(profileIdentifierB)?.reason) +}) diff --git a/src/database/group.ts b/src/database/group.ts index 4a11e0f36b76..7d8fd5cc4802 100644 --- a/src/database/group.ts +++ b/src/database/group.ts @@ -135,11 +135,13 @@ export async function updateUserGroupDatabase( nextRecord = type(orig) || orig } await t.objectStore('groups').put(GroupRecordIntoDB(nextRecord)) - nonDuplicateNewMembers.length && + + if (process.env.NODE_ENV !== 'test' && nonDuplicateNewMembers.length) { MessageCenter.emit('joinGroup', { group: group.identifier, newMembers: nonDuplicateNewMembers, }) + } } /** diff --git a/src/extension/options-page/DashboardComponents/AbstractTab.tsx b/src/extension/options-page/DashboardComponents/AbstractTab.tsx index 8b702da81363..c0a61516051c 100644 --- a/src/extension/options-page/DashboardComponents/AbstractTab.tsx +++ b/src/extension/options-page/DashboardComponents/AbstractTab.tsx @@ -73,7 +73,7 @@ export default function AbstractTab(props: AbstractTabProps) { textColor="primary" variant="fullWidth"> {tabs.map(tab => ( - + ))} @@ -83,6 +83,7 @@ export default function AbstractTab(props: AbstractTabProps) { height={height} value={value} index={index} + key={tab.label} p={tab.p}> {tab.component} diff --git a/src/manifest.json b/src/manifest.json index 33a080d0b28f..721bc856a1fb 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -1,7 +1,7 @@ { "$schema": "http://json.schemastore.org/chrome-manifest", "name": "Maskbook", - "version": "1.10.9", + "version": "1.10.10", "manifest_version": 2, "web_accessible_resources": ["*.css", "*.js", "*.jpg", "*.png"], "permissions": ["storage", "downloads", "webNavigation", "activeTab"], diff --git a/src/plugins/Wallet/UI/Dashboard/Dialogs/Wallet.tsx b/src/plugins/Wallet/UI/Dashboard/Dialogs/Wallet.tsx index fb97d2298eba..59928d59932b 100644 --- a/src/plugins/Wallet/UI/Dashboard/Dialogs/Wallet.tsx +++ b/src/plugins/Wallet/UI/Dashboard/Dialogs/Wallet.tsx @@ -295,16 +295,21 @@ export function WalletRedPacketDetailDialog(props: WalletRedPacketDetailDialogPr const classes = useRedPacketDetailStyles() const sayThanks = () => { - const user = redPacket._found_in_url_?.match(/(?!\/)[\d\w]+(?=\/status)/) - const text = `I just received a Red Packet${ - user ? ` from @${user}` : '' - }. Follow @realMaskbook (maskbook.com) to get your first Twitter #RedPacket. + // TODO: facebook + if (!redPacket._found_in_url_!.includes('twitter.com/')) { + window.open(redPacket._found_in_url_, '_blank', 'noopener noreferrer') + } else { + const user = redPacket._found_in_url_!.match(/(?!\/)[\d\w]+(?=\/status)/) + const text = `I just received a Red Packet${ + user ? ` from @${user}` : '' + }. Follow @realMaskbook (maskbook.com) to get your first Twitter #RedPacket. #maskbook #makerdao ${redPacket._found_in_url_}` - window.open( - `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}`, - '_blank', - 'noopener noreferrer', - ) + window.open( + `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}`, + '_blank', + 'noopener noreferrer', + ) + } } return ( diff --git a/src/plugins/Wallet/UI/RedPacket/RedPacketInDecryptedPost.tsx b/src/plugins/Wallet/UI/RedPacket/RedPacketInDecryptedPost.tsx index 3e3269ece222..1c8cc0696fc1 100644 --- a/src/plugins/Wallet/UI/RedPacket/RedPacketInDecryptedPost.tsx +++ b/src/plugins/Wallet/UI/RedPacket/RedPacketInDecryptedPost.tsx @@ -9,7 +9,7 @@ import { RedPacketWithState } from '../Dashboard/Components/RedPacket' import { RedPacketRecord, RedPacketStatus, WalletRecord } from '../../database/types' import Services from '../../../../extension/service' import { PostIdentifier, ProfileIdentifier } from '../../../../database/type' -import { getPostUrl } from '../../../../social-network-provider/twitter.com/utils/url' + import { withMobileDialog, Dialog, @@ -33,6 +33,7 @@ import { DialogDismissIconUI } from '../../../../components/InjectedComponents/D import { PortalShadowRoot } from '../../../../utils/jss/ShadowRootPortal' import { useI18N } from '../../../../utils/i18n-next-ui' import ShadowRootDialog from '../../../../utils/jss/ShadowRootDialog' +import { getPostUrl } from '../../../../social-network/utils/getPostUrl' const useStyles = makeStyles(theme => createStyles({ diff --git a/src/social-network-provider/facebook.com/UI/collectPosts.tsx b/src/social-network-provider/facebook.com/UI/collectPosts.tsx index da6f4d9a0fe0..2dcc88b9fe4a 100644 --- a/src/social-network-provider/facebook.com/UI/collectPosts.tsx +++ b/src/social-network-provider/facebook.com/UI/collectPosts.tsx @@ -19,7 +19,10 @@ export function collectPostsFacebook(this: SocialNetworkUI) { .closest('.userContentWrapper, [data-store]') // ? inject after comments - const commentSelectorPC = root.clone().querySelectorAll('[data-testid="UFI2Comment/body"]') + const commentSelectorPC = root + .clone() + .querySelectorAll('[role=article] [data-ft] > div > a + span') + .closest(2) const commentSelectorMobile = root .clone() .map(x => x.parentElement) diff --git a/src/social-network-provider/twitter.com/utils/url.ts b/src/social-network-provider/twitter.com/utils/url.ts index 7180be16bc7d..647c02d54ad6 100644 --- a/src/social-network-provider/twitter.com/utils/url.ts +++ b/src/social-network-provider/twitter.com/utils/url.ts @@ -11,14 +11,14 @@ export const twitterUrl = { export const hostLeadingUrlAutoTwitter = (isMobile: boolean) => isMobile ? twitterUrl.hostLeadingUrlMobile : twitterUrl.hostLeadingUrl -export const getPostUrl = (post: PostIdentifier, isMobile: boolean = false) => { +export const getPostUrlAtTwitter = (post: PostIdentifier, isMobile: boolean = false) => { if (!usernameValidator(post.identifier.userId)) { throw new Error(i18n.t('service_username_invalid')) } return `${hostLeadingUrlAutoTwitter(isMobile)}/${post.identifier.userId}/status/${post.postId}` } -export const getProfileUrl = (self: ProfileIdentifier, isMobile: boolean = false) => { +export const getProfileUrlAtTwitter = (self: ProfileIdentifier, isMobile: boolean = false) => { return isMobile ? `${hostLeadingUrlAutoTwitter(isMobile)}/account` : `${hostLeadingUrlAutoTwitter(isMobile)}/${self.userId}` diff --git a/src/social-network-provider/twitter.com/worker/fetch.ts b/src/social-network-provider/twitter.com/worker/fetch.ts index 4c09b07dacd7..4d757a903f47 100644 --- a/src/social-network-provider/twitter.com/worker/fetch.ts +++ b/src/social-network-provider/twitter.com/worker/fetch.ts @@ -1,5 +1,5 @@ import { ProfileIdentifier, PostIdentifier } from '../../../database/type' -import { getPostUrl, getProfileUrl } from '../utils/url' +import { getPostUrlAtTwitter, getProfileUrlAtTwitter } from '../utils/url' import tasks from '../../../extension/content-script/tasks' import { isMobileTwitter } from '../utils/isMobile' @@ -11,9 +11,9 @@ import { isMobileTwitter } from '../utils/isMobile' */ export const fetchPostContent = (post: PostIdentifier) => { - return tasks(getPostUrl(post)).getPostContent() + return tasks(getPostUrlAtTwitter(post)).getPostContent() } export const fetchProfile = (self: ProfileIdentifier) => { - return tasks(getProfileUrl(self, isMobileTwitter as boolean), {}).getProfile(self) + return tasks(getProfileUrlAtTwitter(self, isMobileTwitter as boolean), {}).getProfile(self) } diff --git a/src/social-network-provider/twitter.com/worker/tasks.ts b/src/social-network-provider/twitter.com/worker/tasks.ts index 584eb7936abb..29cf94b9e8eb 100644 --- a/src/social-network-provider/twitter.com/worker/tasks.ts +++ b/src/social-network-provider/twitter.com/worker/tasks.ts @@ -1,10 +1,10 @@ import { ProfileIdentifier } from '../../../database/type' import tasks from '../../../extension/content-script/tasks' -import { getProfileUrl, twitterUrl } from '../utils/url' +import { getProfileUrlAtTwitter, twitterUrl } from '../utils/url' import { i18n } from '../../../utils/i18n-next' export const autoVerifyBio = (self: ProfileIdentifier, prove: string) => { - tasks(getProfileUrl(self), { + tasks(getProfileUrlAtTwitter(self), { active: true, autoClose: false, memorable: false, diff --git a/src/social-network/defaults/taskStartImmersiveSetupDefault.tsx b/src/social-network/defaults/taskStartImmersiveSetupDefault.tsx index dd08791c53c4..f93e20645175 100644 --- a/src/social-network/defaults/taskStartImmersiveSetupDefault.tsx +++ b/src/social-network/defaults/taskStartImmersiveSetupDefault.tsx @@ -5,7 +5,7 @@ import { ImmersiveSetupStepper, ImmersiveSetupStepperUIProps, } from '../../components/InjectedComponents/ImmersiveSetup/SetupStepper' -import { DraggablePaper } from '../../components/InjectedComponents/ImmersiveSetup/DraggablePaper' +import { DraggableDiv } from '../../components/InjectedComponents/ImmersiveSetup/DraggableDiv' import Services from '../../extension/service' import { ValueRef } from '@holoflows/kit/es' import { useValueRef } from '../../utils/hooks/useValueRef' @@ -22,13 +22,13 @@ function UI({ >) { const provePost = useValueRef(post) return ( - + - + ) } let mounted = false diff --git a/src/social-network/ui.ts b/src/social-network/ui.ts index 459fcd09f330..09b174d54862 100644 --- a/src/social-network/ui.ts +++ b/src/social-network/ui.ts @@ -300,7 +300,6 @@ export function activateSocialNetworkUI(): void { { // Do i18nOverwrite for (const lng in ui.i18nOverwrite) { - console.log('Applying ', lng, ui.i18nOverwrite[lng]) i18nNextInstance.addResourceBundle(lng, 'translation', ui.i18nOverwrite[lng], true, true) } } diff --git a/src/social-network/utils/getPostUrl.ts b/src/social-network/utils/getPostUrl.ts new file mode 100644 index 000000000000..2b0d73d5bd98 --- /dev/null +++ b/src/social-network/utils/getPostUrl.ts @@ -0,0 +1,14 @@ +import { PostIdentifier, ProfileIdentifier } from '../../database/type' +import { getPostUrlAtFacebook } from '../../social-network-provider/facebook.com/parse-username' +import { getPostUrlAtTwitter } from '../../social-network-provider/twitter.com/utils/url' + +export function getPostUrl(identifier: PostIdentifier) { + switch (identifier.identifier.network) { + case 'facebook.com': + return getPostUrlAtFacebook(identifier, 'open') + case 'twitter.com': + return getPostUrlAtTwitter(identifier, false) + default: + throw new Error('unknown identifier') + } +} diff --git a/src/stories/Immersive-Setup.tsx b/src/stories/Immersive-Setup.tsx index 97b57858c336..589f79374ded 100644 --- a/src/stories/Immersive-Setup.tsx +++ b/src/stories/Immersive-Setup.tsx @@ -8,19 +8,22 @@ import { text } from '@storybook/addon-knobs' import { sleep } from '@holoflows/kit/es/util/sleep' import { action } from '@storybook/addon-actions' import { ECKeyIdentifier } from '../database/type' +import { DraggableDiv } from '../components/InjectedComponents/ImmersiveSetup/DraggableDiv' storiesOf('Immersive Setup', module).add('Stepper', () => ( - { - action('loadProfile')() - await sleep(700) - }} - provePost={text('Prove post', '🎭A81Kg7HVsITcftN/0IBp2q6+IyfZCYHntkVsMTRl741L0🎭')} - onClose={action('close')} - autoPasteProvePost={async () => { - action('autoPasteProvePost')() - await sleep(700) - }} - /> + + { + action('loadProfile')() + await sleep(700) + }} + provePost={text('Prove post', '🎭A81Kg7HVsITcftN/0IBp2q6+IyfZCYHntkVsMTRl741L0🎭')} + onClose={action('close')} + autoPasteProvePost={async () => { + action('autoPasteProvePost')() + await sleep(700) + }} + /> + )) diff --git a/src/utils/__tests__/hooks/useCaptureEvents.tsx b/src/utils/__tests__/hooks/useCaptureEvents.tsx index a209267acb12..4abc8fcee597 100644 --- a/src/utils/__tests__/hooks/useCaptureEvents.tsx +++ b/src/utils/__tests__/hooks/useCaptureEvents.tsx @@ -4,7 +4,7 @@ import Adapter from 'enzyme-adapter-react-16' configure({ adapter: new Adapter() }) import React, { createRef, RefObject } from 'react' -import { renderHook } from '@testing-library/react-hooks' +import { renderHook, act } from '@testing-library/react-hooks' import { useCapturedInput, captureEevnts } from '../../hooks/useCapturedEvents' const containerRef: RefObject = createRef() @@ -20,7 +20,8 @@ beforeEach(() => { test('invoke callback with input value', () => { const inputSpy = jasmine.createSpy() - renderHook(() => useCapturedInput(inputSpy, [], inputRef)) + const [_, updateInputNode] = renderHook(() => useCapturedInput(inputSpy, [])).result.current + act(() => updateInputNode(inputRef.current)) inputRef.current!.dispatchEvent(new CustomEvent('input', { bubbles: true })) expect(inputSpy.calls.argsFor(0)).toStrictEqual(['']) @@ -42,7 +43,8 @@ for (const name of captureEevnts) { }) test(`capture event: ${name}`, () => { const containerSpy = jasmine.createSpy() - renderHook(() => useCapturedInput(() => {}, [], inputRef)) + const [_, updateInputNode] = renderHook(() => useCapturedInput(() => {}, [])).result.current + act(() => updateInputNode(inputRef.current)) containerRef.current!.addEventListener(name, containerSpy) inputRef.current!.dispatchEvent(new CustomEvent(name, { bubbles: true })) @@ -52,12 +54,14 @@ for (const name of captureEevnts) { }) test(`remove listener: ${name}`, () => { const containerSpy = jasmine.createSpy() - containerRef.current!.addEventListener(name, containerSpy) inputRef.current!.dispatchEvent(new CustomEvent(name, { bubbles: true })) expect(containerSpy.calls.count()).toBe(1) - const hook = renderHook(() => useCapturedInput(() => {}, [], inputRef)) + const hook = renderHook(() => useCapturedInput(() => {}, [])) + const [_, updateInputNode] = hook.result.current + act(() => updateInputNode(inputRef.current)) + inputRef.current!.dispatchEvent(new CustomEvent(name, { bubbles: true })) expect(containerSpy.calls.count()).toBe(1) diff --git a/src/utils/__tests__/hooks/useQRCodeScan.tsx b/src/utils/__tests__/hooks/useQRCodeScan.tsx index 7de8833d7ea0..827bdd0f14cf 100644 --- a/src/utils/__tests__/hooks/useQRCodeScan.tsx +++ b/src/utils/__tests__/hooks/useQRCodeScan.tsx @@ -71,7 +71,7 @@ test('scan succeeded', async () => { await hook.waitForNextUpdate() expect(onResultSpy.calls.count()).toBe(0) - await sleep(200) + await sleep(1000) expect(onResultSpy.calls.count() > 0).toBeTruthy() }) @@ -89,6 +89,6 @@ test('scan failed', async () => { await hook.waitForNextUpdate() expect(onErrorSpy.calls.count()).toBe(0) - await sleep(1200) + await sleep(2000) expect(onErrorSpy.calls.count() > 0).toBeTruthy() }) diff --git a/src/utils/hooks/useCapturedEvents.ts b/src/utils/hooks/useCapturedEvents.ts index 09e5a952aa1d..bcf0566e5d27 100644 --- a/src/utils/hooks/useCapturedEvents.ts +++ b/src/utils/hooks/useCapturedEvents.ts @@ -1,6 +1,5 @@ /* eslint-disable react-hooks/exhaustive-deps */ import { useEffect, useCallback } from 'react' -import { or } from '../../components/custom-ui-helper' import React from 'react' export const captureEevnts: (keyof HTMLElementEventMap)[] = [ @@ -19,37 +18,43 @@ export const captureEevnts: (keyof HTMLElementEventMap)[] = [ 'change', ] +function binder( + node: HTMLInputElement | null, + keys: T[], + fn: (e: HTMLElementEventMap[T]) => void, +) { + let current: HTMLInputElement | null + const bind = (input: HTMLInputElement) => keys.forEach(k => input.addEventListener(k, fn, true)) + const unbind = (input: HTMLInputElement) => keys.forEach(k => input.removeEventListener(k, fn, true)) + return () => { + if (!node) return + current = node + bind(current) + return () => { + if (!current) return + unbind(current) + current = null + } + } +} + /** * ! Call this hook inside Shadow Root! */ -export function useCapturedInput( - onChange: (newVal: string) => void, - deps: any[] = [], - _ref?: React.MutableRefObject, -) { - const ref = or(_ref, React.useRef(null)) +export function useCapturedInput(onChange: (newVal: string) => void, deps: any[] = []) { + const [node, setNode] = React.useState(null) + const ref = useCallback((nextNode: HTMLInputElement | null) => setNode(nextNode), []) const stop = useCallback((e: Event) => e.stopPropagation(), deps) const use = useCallback( (e: Event) => onChange((e.currentTarget as HTMLInputElement)?.value ?? (e.target as HTMLInputElement)?.value), [onChange].concat(deps), ) - function binder(keys: T[], fn: (e: HTMLElementEventMap[T]) => void) { - let current: HTMLInputElement | null - const bind = (input: HTMLInputElement) => keys.forEach(k => input.addEventListener(k, fn, true)) - const unbind = (input: HTMLInputElement) => keys.forEach(k => input.removeEventListener(k, fn, true)) - - return () => { - if (!ref.current) return - current = ref.current - bind(current) - return () => { - if (!current) return - unbind(current) - current = null - } - } - } - useEffect(binder(['input'], use), [ref.current].concat(deps)) - useEffect(binder(captureEevnts, stop), [ref.current].concat(deps)) - return [binder, ref] as const + useEffect(binder(node, ['input'], use), [node].concat(deps)) + useEffect(binder(node, captureEevnts, stop), [node].concat(deps)) + return [ + (keys: T[], fn: (e: HTMLElementEventMap[T]) => void) => + binder(node, keys, fn), + ref, + node, + ] as const } diff --git a/src/utils/type-transform/BackupFileShortRepresentation.ts b/src/utils/type-transform/BackupFileShortRepresentation.ts index 80b1f631fb59..68a36915aa4d 100644 --- a/src/utils/type-transform/BackupFileShortRepresentation.ts +++ b/src/utils/type-transform/BackupFileShortRepresentation.ts @@ -32,7 +32,7 @@ export function compressBackupFile(file: BackupJSONFileLatest, profileIdentifier profileIdentifier.userId, nickname, localKey?.k || - profiles.filter(x => x.linkedPersona === profileIdentifier!.toText()).filter(x => x.localKey)[0]?.localKey + profiles.filter(x => x.identifier === profileIdentifier!.toText()).filter(x => x.localKey)[0]?.localKey ?.k || '', compressSecp256k1Key(privateKey, 'private'), diff --git a/tsconfig.json b/tsconfig.json index c684853ca76e..03e02c4a6f0e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ "target": "ES2019" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, - "module": "ESNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, + "module": "CommonJS" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, "lib": ["DOM", "DOM.Iterable", "ES2019"] /* Specify library files to be included in the compilation. */, "allowJs": true /* Allow javascript files to be compiled. */, // "checkJs": true, /* Report errors in .js files. */ diff --git a/webpack.config.js b/webpack.config.js index e5717c8448a9..c390ce2146fd 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -34,6 +34,7 @@ const ManifestGeneratorPlugin = require('webpack-extension-manifest-plugin') * --firefox-gecko * --chromium * --wk-webview + * --e2e */ const calcTarget = argv => ({ /** @type {'nightly' | boolean} */ @@ -48,6 +49,8 @@ const calcTarget = argv => ({ Chromium: (argv.chromium), /** @type {boolean} */ WKWebview: (argv['wk-webview']), + /** @type {boolean} */ + E2E: (argv.e2e), }) /** @@ -219,7 +222,7 @@ module.exports = (argvEnv, argv) => { // Setting conditional compilations { - /** @type {'Chromium' | 'Firefox' | 'WKWebview' | undefined} */ + /** @type {'Chromium' | 'Firefox' | 'WKWebview' | 'E2E' | undefined} */ let buildTarget = undefined /** @type {'android' | 'desktop' | 'GeckoView' | undefined} */ let firefoxVariant = undefined @@ -229,6 +232,7 @@ module.exports = (argvEnv, argv) => { if (target.FirefoxForAndroid) firefoxVariant = 'android' if (target.StandaloneGeckoView) firefoxVariant = 'GeckoView' if (target.WKWebview) buildTarget = 'WKWebview' + if (target.E2E) buildTarget = 'E2E' if (buildTarget) config.plugins.push( new webpack.DefinePlugin({ @@ -252,7 +256,7 @@ module.exports = (argvEnv, argv) => { if (target.FirefoxForAndroid) modifiers.firefox(manifest) if (target.StandaloneGeckoView) modifiers.geckoview(manifest) if (target.WKWebview) modifiers.WKWebview(manifest) - if (env === 'development') modifiers.development(manifest, target) + if (env === 'development' || target.E2E) modifiers.development(manifest, target) else modifiers.production(manifest, target) config.plugins.push(new ManifestGeneratorPlugin({ config: { base: manifest } })) @@ -303,6 +307,7 @@ function addTSLoader() { options: { transpileOnly: true, compilerOptions: { + module: 'esnext', jsx: 'react', noEmit: false, }, diff --git a/yarn.lock b/yarn.lock index e0deb7856697..ed0a3e26f268 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1930,6 +1930,38 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== +"@hapi/address@2.x.x": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5" + integrity sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ== + +"@hapi/bourne@1.x.x": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-1.3.2.tgz#0a7095adea067243ce3283e1b56b8a8f453b242a" + integrity sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA== + +"@hapi/hoek@8.x.x", "@hapi/hoek@^8.3.0": + version "8.5.1" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-8.5.1.tgz#fde96064ca446dec8c55a8c2f130957b070c6e06" + integrity sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow== + +"@hapi/joi@^15.0.3": + version "15.1.1" + resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-15.1.1.tgz#c675b8a71296f02833f8d6d243b34c57b8ce19d7" + integrity sha512-entf8ZMOK8sc+8YfeOlM8pCfg3b5+WZIKBfUaaJT8UsjAAPjartzxIYm3TIbjvA4u+u++KbcXD38k682nVHDAQ== + dependencies: + "@hapi/address" "2.x.x" + "@hapi/bourne" "1.x.x" + "@hapi/hoek" "8.x.x" + "@hapi/topo" "3.x.x" + +"@hapi/topo@3.x.x": + version "3.1.6" + resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-3.1.6.tgz#68d935fa3eae7fdd5ab0d7f953f3205d8b2bfc29" + integrity sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ== + dependencies: + "@hapi/hoek" "^8.3.0" + "@holoflows/kit@https://github.com/DimensionDev/holoflows-kit": version "0.6.1" resolved "https://github.com/DimensionDev/holoflows-kit#5b2113f7bfc2e235ae6d6987d4d44c4df61ab5fb" @@ -2014,7 +2046,7 @@ slash "^3.0.0" strip-ansi "^6.0.0" -"@jest/environment@^24.3.0": +"@jest/environment@^24", "@jest/environment@^24.3.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-24.9.0.tgz#21e3afa2d65c0586cbd6cbefe208bafade44ab18" integrity sha512-5A1QluTPhvdIPFYnO3sZC3smkNeXPVELz7ikPbhUj0bQjB07EoE9qtLrem14ZUYWdVayYbsjVwIiL4WBIMV4aQ== @@ -2033,7 +2065,7 @@ "@jest/types" "^25.1.0" jest-mock "^25.1.0" -"@jest/fake-timers@^24.3.0", "@jest/fake-timers@^24.9.0": +"@jest/fake-timers@^24", "@jest/fake-timers@^24.3.0", "@jest/fake-timers@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-24.9.0.tgz#ba3e6bf0eecd09a636049896434d306636540c93" integrity sha512-eWQcNa2YSwzXWIMC5KufBh3oWRIijrQFROsIqt6v/NS9Io/gknw1jsAC9c+ih/RQX4A3O7SeWAhQeN0goKhT9A== @@ -2178,7 +2210,7 @@ source-map "^0.6.1" write-file-atomic "^3.0.0" -"@jest/types@^24.3.0", "@jest/types@^24.9.0": +"@jest/types@^24", "@jest/types@^24.3.0", "@jest/types@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.9.0.tgz#63cb26cb7500d069e5a389441a7c6ab5e909fc59" integrity sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw== @@ -3464,6 +3496,14 @@ resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== +"@types/expect-puppeteer@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@types/expect-puppeteer/-/expect-puppeteer-4.4.0.tgz#8e1ee1e7ed7e12f36efcfe81c17d4d6b817cb3d2" + integrity sha512-j2zKlMms/qrdr5clPWWnhTqFGc2HFWv465gowMmCfBkorrFhDXpYgTduJRFSAwYcmNOopK5pgNXKb7dwon8pqQ== + dependencies: + "@types/jest" "*" + "@types/puppeteer" "*" + "@types/glob@^7.1.1": version "7.1.1" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" @@ -3508,6 +3548,25 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" +"@types/jest-environment-puppeteer@^4.3.1": + version "4.3.1" + resolved "https://registry.yarnpkg.com/@types/jest-environment-puppeteer/-/jest-environment-puppeteer-4.3.1.tgz#597d16fa0594a5daf1b3c08262bfbb011e146c2d" + integrity sha512-e7G12WRw525gsiz7NW3tKY+YWyNR08r3QvyC31rzMrnn7CkyGbsiskBjnR3koY/6jwIASgxO5knm4xJQ1KtbQQ== + dependencies: + "@jest/environment" "^24" + "@jest/fake-timers" "^24" + "@jest/types" "^24" + "@types/puppeteer" "*" + jest-mock "^24" + +"@types/jest@*": + version "25.1.3" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-25.1.3.tgz#9b0b5addebccfb631175870be8ba62182f1bc35a" + integrity sha512-jqargqzyJWgWAJCXX96LBGR/Ei7wQcZBvRv0PLEu9ZByMfcs23keUJrKv9FMR6YZf9YCbfqDqgmY+JUBsnqhrg== + dependencies: + jest-diff "^25.1.0" + pretty-format "^25.1.0" + "@types/jest@^25.1.2": version "25.1.2" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-25.1.2.tgz#1c4c8770c27906c7d8def5d2033df9dbd39f60da" @@ -3607,6 +3666,13 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== +"@types/puppeteer@*", "@types/puppeteer@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/puppeteer/-/puppeteer-2.0.0.tgz#82c04f93367e2d3396e371a71be1167332148838" + integrity sha512-QPHXIcaPcijMbvizoM7PRL97Rm+aM8J2DmgTz2tt79b15PqbyeaCppYonvPLHQ/Q5ea92BUHDpv4bsqtiTy8kQ== + dependencies: + "@types/node" "*" + "@types/q@^1.5.1": version "1.5.2" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" @@ -3767,12 +3833,12 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@^2.20.0": - version "2.20.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.20.0.tgz#a522d0e1e4898f7c9c6a8e1ed3579b60867693fa" - integrity sha512-cimIdVDV3MakiGJqMXw51Xci6oEDEoPkvh8ggJe2IIzcc0fYqAxOXN6Vbeanahz6dLZq64W+40iUEc9g32FLDQ== +"@typescript-eslint/eslint-plugin@^2.22.0": + version "2.22.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.22.0.tgz#218ce6d4aa0244c6a40baba39ca1e021b26bb017" + integrity sha512-BvxRLaTDVQ3N+Qq8BivLiE9akQLAOUfxNHIEhedOcg8B2+jY8Rc4/D+iVprvuMX1AdezFYautuGDwr9QxqSxBQ== dependencies: - "@typescript-eslint/experimental-utils" "2.20.0" + "@typescript-eslint/experimental-utils" "2.22.0" eslint-utils "^1.4.3" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" @@ -3787,13 +3853,13 @@ "@typescript-eslint/typescript-estree" "2.19.2" eslint-scope "^5.0.0" -"@typescript-eslint/experimental-utils@2.20.0": - version "2.20.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.20.0.tgz#3b6fa5a6b8885f126d5a4280e0d44f0f41e73e32" - integrity sha512-fEBy9xYrwG9hfBLFEwGW2lKwDRTmYzH3DwTmYbT+SMycmxAoPl0eGretnBFj/s+NfYBG63w/5c3lsvqqz5mYag== +"@typescript-eslint/experimental-utils@2.22.0": + version "2.22.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.22.0.tgz#4d00c91fbaaa68e56e7869be284999a265707f85" + integrity sha512-sJt1GYBe6yC0dWOQzXlp+tiuGglNhJC9eXZeC8GBVH98Zv9jtatccuhz0OF5kC/DwChqsNfghHx7OlIDQjNYAQ== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "2.20.0" + "@typescript-eslint/typescript-estree" "2.22.0" eslint-scope "^5.0.0" "@typescript-eslint/parser@^2.19.2": @@ -3819,10 +3885,10 @@ semver "^6.3.0" tsutils "^3.17.1" -"@typescript-eslint/typescript-estree@2.20.0": - version "2.20.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.20.0.tgz#90a0f5598826b35b966ca83483b1a621b1a4d0c9" - integrity sha512-WlFk8QtI8pPaE7JGQGxU7nGcnk1ccKAJkhbVookv94ZcAef3m6oCE/jEDL6dGte3JcD7reKrA0o55XhBRiVT3A== +"@typescript-eslint/typescript-estree@2.22.0": + version "2.22.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.22.0.tgz#a16ed45876abf743e1f5857e2f4a1c3199fd219e" + integrity sha512-2HFZW2FQc4MhIBB8WhDm9lVFaBDy6h9jGrJ4V2Uzxe/ON29HCHBTj3GkgcsgMWfsl2U5as+pTOr30Nibaw7qRQ== dependencies: debug "^4.1.1" eslint-visitor-keys "^1.1.0" @@ -6363,15 +6429,20 @@ commander@2.9.0: dependencies: graceful-readlink ">= 1.0.0" +commander@^2.11.0, commander@^2.9.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + commander@^2.19.0, commander@^2.20.0, commander@^2.3.0, commander@^2.6.0, commander@~2.20.0: version "2.20.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== -commander@^2.9.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +commander@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e" + integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow== commander@^4.0.0: version "4.1.1" @@ -6996,6 +7067,14 @@ currently-unhandled@^0.4.1: dependencies: array-find-index "^1.0.1" +cwd@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/cwd/-/cwd-0.10.0.tgz#172400694057c22a13b0cf16162c7e4b7a7fe567" + integrity sha1-FyQAaUBXwioTsM8WFix+S3p/5Wc= + dependencies: + find-pkg "^0.1.2" + fs-exists-sync "^0.1.0" + cyclist@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" @@ -8626,6 +8705,13 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" +expand-tilde@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-1.2.2.tgz#0b81eba897e5a3d31d1c3d102f8f01441e559449" + integrity sha1-C4HrqJflo9MdHD0QL48BRB5VlEk= + dependencies: + os-homedir "^1.0.1" + expand-tilde@^2.0.0, expand-tilde@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" @@ -8633,6 +8719,11 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" +expect-puppeteer@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/expect-puppeteer/-/expect-puppeteer-4.4.0.tgz#1c948af08acdd6c8cbdb7f90e617f44d86888886" + integrity sha512-6Ey4Xy2xvmuQu7z7YQtMsaMV0EHJRpVxIDOd5GRrm04/I3nkTKIutELfECsLp6le+b3SSa3cXhPiw6PgqzxYWA== + expect@^25.1.0: version "25.1.0" resolved "https://registry.yarnpkg.com/expect/-/expect-25.1.0.tgz#7e8d7b06a53f7d66ec927278db3304254ee683ee" @@ -9052,6 +9143,30 @@ find-cache-dir@^3.2.0: make-dir "^3.0.0" pkg-dir "^4.1.0" +find-file-up@^0.1.2: + version "0.1.3" + resolved "https://registry.yarnpkg.com/find-file-up/-/find-file-up-0.1.3.tgz#cf68091bcf9f300a40da411b37da5cce5a2fbea0" + integrity sha1-z2gJG8+fMApA2kEbN9pczlovvqA= + dependencies: + fs-exists-sync "^0.1.0" + resolve-dir "^0.1.0" + +find-pkg@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/find-pkg/-/find-pkg-0.1.2.tgz#1bdc22c06e36365532e2a248046854b9788da557" + integrity sha1-G9wiwG42NlUy4qJIBGhUuXiNpVc= + dependencies: + find-file-up "^0.1.2" + +find-process@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/find-process/-/find-process-1.4.3.tgz#25f9105dc32e42abad4636752c37c51cd57dce45" + integrity sha512-+IA+AUsQCf3uucawyTwMWcY+2M3FXq3BRvw3S+j5Jvydjk31f/+NPWpYZOJs+JUs2GvxH4Yfr6Wham0ZtRLlPA== + dependencies: + chalk "^2.0.1" + commander "^2.11.0" + debug "^2.6.8" + find-replace@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-1.0.3.tgz#b88e7364d2d9c959559f388c66670d6130441fa0" @@ -9300,6 +9415,11 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== +fs-exists-sync@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add" + integrity sha1-mC1ok6+RjnLQjeyehnP/K1qNat0= + fs-extra@^0.30.0: version "0.30.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.30.0.tgz#f233ffcc08d4da7d432daa449776989db1df93f0" @@ -9644,6 +9764,14 @@ global-modules@2.0.0: dependencies: global-prefix "^3.0.0" +global-modules@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-0.2.3.tgz#ea5a3bed42c6d6ce995a4f8a1269b5dae223828d" + integrity sha1-6lo77ULG1s6ZWk+KEmm12uIjgo0= + dependencies: + global-prefix "^0.1.4" + is-windows "^0.2.0" + global-modules@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" @@ -9653,6 +9781,16 @@ global-modules@^1.0.0: is-windows "^1.0.1" resolve-dir "^1.0.0" +global-prefix@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-0.1.5.tgz#8d3bc6b8da3ca8112a160d8d496ff0462bfef78f" + integrity sha1-jTvGuNo8qBEqFg2NSW/wRiv+948= + dependencies: + homedir-polyfill "^1.0.0" + ini "^1.3.4" + is-windows "^0.2.0" + which "^1.2.12" + global-prefix@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" @@ -10107,7 +10245,7 @@ hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.2.1, hoist-non-react- dependencies: react-is "^16.7.0" -homedir-polyfill@^1.0.1: +homedir-polyfill@^1.0.0, homedir-polyfill@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== @@ -11218,6 +11356,11 @@ is-window@^1.0.2: resolved "https://registry.yarnpkg.com/is-window/-/is-window-1.0.2.tgz#2c896ca53db97de45d3c33133a65d8c9f563480d" integrity sha1-LIlspT25feRdPDMTOmXYyfVjSA0= +is-windows@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c" + integrity sha1-3hqm1j6indJIc3tp8f+LgALSEIw= + is-windows@^1.0.1, is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -11447,6 +11590,19 @@ jest-config@^25.1.0: pretty-format "^25.1.0" realpath-native "^1.1.0" +jest-dev-server@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/jest-dev-server/-/jest-dev-server-4.4.0.tgz#557113faae2877452162696aa94c1e44491ab011" + integrity sha512-STEHJ3iPSC8HbrQ3TME0ozGX2KT28lbT4XopPxUm2WimsX3fcB3YOptRh12YphQisMhfqNSNTZUmWyT3HEXS2A== + dependencies: + chalk "^3.0.0" + cwd "^0.10.0" + find-process "^1.4.3" + prompts "^2.3.0" + spawnd "^4.4.0" + tree-kill "^1.2.2" + wait-on "^3.3.0" + jest-diff@^25.1.0: version "25.1.0" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.1.0.tgz#58b827e63edea1bc80c1de952b80cec9ac50e1ad" @@ -11510,6 +11666,16 @@ jest-environment-node@^25.1.0: jest-mock "^25.1.0" jest-util "^25.1.0" +jest-environment-puppeteer@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/jest-environment-puppeteer/-/jest-environment-puppeteer-4.4.0.tgz#d82a37e0e0c51b63cc6b15dea101d53967508860" + integrity sha512-iV8S8+6qkdTM6OBR/M9gKywEk8GDSOe05hspCs5D8qKSwtmlUfdtHfB4cakdc68lC6YfK3AUsLirpfgodCHjzQ== + dependencies: + chalk "^3.0.0" + cwd "^0.10.0" + jest-dev-server "^4.4.0" + merge-deep "^3.0.2" + jest-esm-transformer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/jest-esm-transformer/-/jest-esm-transformer-1.0.0.tgz#b6c58f496aa48194f96361a52f5c578fd2209726" @@ -11645,7 +11811,7 @@ jest-message-util@^25.1.0: slash "^3.0.0" stack-utils "^1.0.1" -jest-mock@^24.0.0, jest-mock@^24.9.0: +jest-mock@^24, jest-mock@^24.0.0, jest-mock@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-24.9.0.tgz#c22835541ee379b908673ad51087a2185c13f1c6" integrity sha512-3BEYN5WbSq9wd+SyLDES7AHnjH9A/ROBwmz7l2y+ol+NtSFO8DYiEBzoO1CeFc9a8DYy10EO4dDFVv/wN3zl1w== @@ -11664,6 +11830,14 @@ jest-pnp-resolver@^1.2.1: resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz#ecdae604c077a7fbc70defb6d517c3c1c898923a" integrity sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ== +jest-puppeteer@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/jest-puppeteer/-/jest-puppeteer-4.4.0.tgz#4b906e638a5e3782ed865e7b673c82047b85952e" + integrity sha512-ZaiCTlPZ07B9HW0erAWNX6cyzBqbXMM7d2ugai4epBDKpKvRDpItlRQC6XjERoJELKZsPziFGS0OhhUvTvQAXA== + dependencies: + expect-puppeteer "^4.4.0" + jest-environment-puppeteer "^4.4.0" + jest-regex-util@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-24.9.0.tgz#c13fb3380bde22bf6575432c493ea8fe37965636" @@ -13759,10 +13933,10 @@ node-rsa@^0.4.0: dependencies: asn1 "0.2.3" -node-stego@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/node-stego/-/node-stego-0.10.0.tgz#aebaa517b15281e4f66127f96bb6c2abd4575133" - integrity sha512-IHqDZ1Q6PeT9/AmKiT77+a8BaBaDVDitlsasCQhAjJzhjtDCgC0b43bu5rN/01QEDE7xh5XuOiLNmL5cOwOj9A== +node-stego@^0.10.1: + version "0.10.1" + resolved "https://registry.yarnpkg.com/node-stego/-/node-stego-0.10.1.tgz#5cbbd91c3ef70fa5a5d8fcdb6f4477b82ae5bd02" + integrity sha512-ZMZChU6FrsGvXBoUzMPVhsmkJx/gsPjGwzkHuH1tqBOSsHTX/dC5uQtj+8+w7WpnA7ZkYmdaGbJ6dPA278jE/w== dependencies: meow "^5.0.0" @@ -14184,7 +14358,7 @@ os-browserify@^0.3.0: resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= -os-homedir@^1.0.0: +os-homedir@^1.0.0, os-homedir@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= @@ -15024,6 +15198,14 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.3" +prompts@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.3.1.tgz#b63a9ce2809f106fa9ae1277c275b167af46ea05" + integrity sha512-qIP2lQyCwYbdzcqHIUi2HAxiWixhoM9OdLCWf8txXsapC/X9YdsCoeyRIXE/GP+Q0J37Q7+XN/MFqbUa7IzXNA== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.4" + prop-types-exact@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/prop-types-exact/-/prop-types-exact-1.2.0.tgz#825d6be46094663848237e3925a98c6e944e9869" @@ -15259,10 +15441,10 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -puppeteer-core@^2.1.1: +puppeteer@^2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-2.1.1.tgz#e9b3fbc1237b4f66e25999832229e9db3e0b90ed" - integrity sha512-n13AWriBMPYxnpbb6bnaY5YoY6rGj8vPLrz6CZF3o0qJNEwlcfJVxBzYZ0NJsQ21UbdJoijPCDrM++SUVEz7+w== + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-2.1.1.tgz#ccde47c2a688f131883b50f2d697bd25189da27e" + integrity sha512-LWzaDVQkk1EPiuYeTOj+CZRIjda4k2s5w4MK4xoH2+kgWV/SDlkYHmxatDdtYrciHUKSXTsGgPgPP8ILVdBsxg== dependencies: "@types/mime-types" "^2.1.0" debug "^4.1.0" @@ -15555,25 +15737,15 @@ react-docgen@^5.0.0: node-dir "^0.1.10" strip-indent "^3.0.0" -react-dom@^16.12.0: - version "16.12.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.12.0.tgz#0da4b714b8d13c2038c9396b54a92baea633fe11" - integrity sha512-LMxFfAGrcS3kETtQaCkTKjMiifahaMySFDn71fZUNpPHZQEzmk/GiAeIT8JSOrHB23fnuCOMruL2a8NYlw+8Gw== +react-dom@^16.13.0, react-dom@^16.8.3: + version "16.13.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.0.tgz#cdde54b48eb9e8a0ca1b3dc9943d9bb409b81866" + integrity sha512-y09d2c4cG220DzdlFkPTnVvGTszVvNpC73v+AaLGLHbkpy3SSgvYq8x0rNwPJ/Rk/CicTNgk0hbHNw1gMEZAXg== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.2" - scheduler "^0.18.0" - -react-dom@^16.8.3: - version "16.11.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.11.0.tgz#7e7c4a5a85a569d565c2462f5d345da2dd849af5" - integrity sha512-nrRyIUE1e7j8PaXSPtyRKtz+2y9ubW/ghNgqKFHHAHaeP0fpF5uXR+sq8IMRHC+ZUxw7W9NyCDTBtwWxvkb0iA== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - scheduler "^0.17.0" + scheduler "^0.19.0" react-draggable@^4.0.3: version "4.0.3" @@ -15787,15 +15959,15 @@ react-syntax-highlighter@^8.0.1: prismjs "^1.8.4" refractor "^2.4.1" -react-test-renderer@^16.0.0-0, react-test-renderer@^16.12.0: - version "16.12.0" - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.12.0.tgz#11417ffda579306d4e841a794d32140f3da1b43f" - integrity sha512-Vj/teSqt2oayaWxkbhQ6gKis+t5JrknXfPVo+aIJ8QwYAqMPH77uptOdrlphyxl8eQI/rtkOYg86i/UWkpFu0w== +react-test-renderer@^16.0.0-0, react-test-renderer@^16.13.0: + version "16.13.0" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.13.0.tgz#39ba3bf72cedc8210c3f81983f0bb061b14a3014" + integrity sha512-NQ2S9gdMUa7rgPGpKGyMcwl1d6D9MCF0lftdI3kts6kkiX+qvpC955jNjAZXlIDTjnN9jwFI8A8XhRh/9v0spA== dependencies: object-assign "^4.1.1" prop-types "^15.6.2" react-is "^16.8.6" - scheduler "^0.18.0" + scheduler "^0.19.0" react-textarea-autosize@^7.1.0: version "7.1.0" @@ -15852,10 +16024,10 @@ react-window@^1.8.5: "@babel/runtime" "^7.0.0" memoize-one ">=3.1.1 <6" -react@^16.12.0, react@^16.8.1, react@^16.8.3: - version "16.12.0" - resolved "https://registry.yarnpkg.com/react/-/react-16.12.0.tgz#0c0a9c6a142429e3614834d5a778e18aa78a0b83" - integrity sha512-fglqy3k5E+81pA8s+7K0/T3DBCF0ZDOher1elBFzF7O6arXJgzyu/FW+COxFvAWXJoJN9KIZbT2LXlukwphYTA== +react@^16.13.0, react@^16.8.1, react@^16.8.3: + version "16.13.0" + resolved "https://registry.yarnpkg.com/react/-/react-16.13.0.tgz#d046eabcdf64e457bbeed1e792e235e1b9934cf7" + integrity sha512-TSavZz2iSLkq5/oiE7gnFzmURKZMltmi193rm5HEoUDAXpzT9Kzw6oNZnGoai/4+fUnm7FqS5dwgUL34TujcWQ== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" @@ -16450,6 +16622,14 @@ resolve-cwd@^3.0.0: dependencies: resolve-from "^5.0.0" +resolve-dir@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-0.1.1.tgz#b219259a5602fac5c5c496ad894a6e8cc430261e" + integrity sha1-shklmlYC+sXFxJatiUpujMQwJh4= + dependencies: + expand-tilde "^1.2.2" + global-modules "^0.2.3" + resolve-dir@^1.0.0, resolve-dir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" @@ -16654,6 +16834,11 @@ rx-lite@^3.1.2: resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" integrity sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI= +rx@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782" + integrity sha1-pfE/957zt0D+MKqAP7CfmIBdR4I= + rxjs@^6.3.3: version "6.5.3" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.3.tgz#510e26317f4db91a7eb1de77d9dd9ba0a4899a3a" @@ -16727,18 +16912,10 @@ saxes@^3.1.9: dependencies: xmlchars "^2.1.1" -scheduler@^0.17.0: - version "0.17.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.17.0.tgz#7c9c673e4ec781fac853927916d1c426b6f3ddfe" - integrity sha512-7rro8Io3tnCPuY4la/NuI5F2yfESpnfZyT6TtkXnSWVkcu0BCDJ+8gk5ozUaFaxpIyNuWAPXrH0yFcSi28fnDA== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - -scheduler@^0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.18.0.tgz#5901ad6659bc1d8f3fdaf36eb7a67b0d6746b1c4" - integrity sha512-agTSHR1Nbfi6ulI0kYNK0203joW2Y5W4po4l+v03tOoiJKpTBbxpNhWDvqc/4IcOw+KLmSiQLTasZ4cab2/UWQ== +scheduler@^0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.0.tgz#a715d56302de403df742f4a9be11975b32f5698d" + integrity sha512-xowbVaTPe9r7y7RUejcK73/j8tt2jfiyTednOvHbA8JoClvMYCp+r8QegLwK/n8zWQAtZb1fFnER4XLBZXrCxA== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" @@ -16752,15 +16929,7 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" -schema-utils@^2.0.0, schema-utils@^2.0.1, schema-utils@^2.5.0: - version "2.6.1" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.6.1.tgz#eb78f0b945c7bcfa2082b3565e8db3548011dc4f" - integrity sha512-0WXHDs1VDJyo+Zqs9TKLKyD/h7yDpHUhEFsM2CzkICFdoX1av+GBq/J2xRTFfsQO5kBfhZzANf2VcIm84jqDbg== - dependencies: - ajv "^6.10.2" - ajv-keywords "^3.4.1" - -schema-utils@^2.6.1, schema-utils@^2.6.4: +schema-utils@^2.0.0, schema-utils@^2.0.1, schema-utils@^2.5.0, schema-utils@^2.6.4: version "2.6.4" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.6.4.tgz#a27efbf6e4e78689d91872ee3ccfa57d7bdd0f53" integrity sha512-VNjcaUxVnEeun6B2fiiUDjXXBtD4ZSH7pdbfIu1pOFwgptDPLMo/z9jr4sUfsjFVPqDCEin/F7IYlq7/E6yDbQ== @@ -17158,6 +17327,11 @@ sisteransi@^1.0.3: resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.3.tgz#98168d62b79e3a5e758e27ae63c4a053d748f4eb" integrity sha512-SbEG75TzH8G7eVXFSN5f9EExILKfly7SUvVY5DhhYLvfhKqhDFY0OzevWa/zwak0RLRfWS5AvfMWpd9gJvr5Yg== +sisteransi@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.4.tgz#386713f1ef688c7c0304dc4c0632898941cad2e3" + integrity sha512-/ekMoM4NJ59ivGSfKapeG+FWtrmWvA1p6FBZwXrqojw90vJu8lBmrTxCMuBCydKtkaUe2zt4PlxeTKpjwMbyig== + slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" @@ -17323,6 +17497,16 @@ spawn-sync@1.0.15: concat-stream "^1.4.7" os-shim "^0.1.2" +spawnd@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/spawnd/-/spawnd-4.4.0.tgz#bb52c5b34a22e3225ae1d3acb873b2cd58af0886" + integrity sha512-jLPOfB6QOEgMOQY15Z6+lwZEhH3F5ncXxIaZ7WHPIapwNNLyjrs61okj3VJ3K6tmP5TZ6cO0VAu9rEY4MD4YQg== + dependencies: + exit "^0.1.2" + signal-exit "^3.0.2" + tree-kill "^1.2.2" + wait-port "^0.2.7" + spdx-correct@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" @@ -18153,21 +18337,7 @@ terser-webpack-plugin@^1.4.3: webpack-sources "^1.4.0" worker-farm "^1.7.0" -terser-webpack-plugin@^2.1.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-2.3.2.tgz#6d3d1b0590c8f729bfbaeb7fb2528b8b62db4c74" - integrity sha512-SmvB/6gtEPv+CJ88MH5zDOsZdKXPS/Uzv2//e90+wM1IHFUhsguPKEILgzqrM1nQ4acRXN/SV4Obr55SXC+0oA== - dependencies: - cacache "^13.0.1" - find-cache-dir "^3.2.0" - jest-worker "^24.9.0" - schema-utils "^2.6.1" - serialize-javascript "^2.1.2" - source-map "^0.6.1" - terser "^4.4.3" - webpack-sources "^1.4.3" - -terser-webpack-plugin@^2.3.4: +terser-webpack-plugin@^2.1.2, terser-webpack-plugin@^2.3.4: version "2.3.4" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-2.3.4.tgz#ac045703bd8da0936ce910d8fb6350d0e1dee5fe" integrity sha512-Nv96Nws2R2nrFOpbzF6IxRDpIkkIfmhvOws+IqMvYdFLO7o6wAILWFKONFgaYy8+T4LVz77DQW0f7wOeDEAjrg== @@ -18492,7 +18662,7 @@ traverse@0.4.x: resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.4.6.tgz#d04b2280e4c792a5815429ef7b8b60c64c9ccc34" integrity sha1-0EsigOTHkqWBVCnve4tgxkyczDQ= -tree-kill@^1.1.0: +tree-kill@^1.1.0, tree-kill@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== @@ -19341,6 +19511,26 @@ w3c-xmlserializer@^1.1.2: webidl-conversions "^4.0.2" xml-name-validator "^3.0.0" +wait-on@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-3.3.0.tgz#9940981d047a72a9544a97b8b5fca45b2170a082" + integrity sha512-97dEuUapx4+Y12aknWZn7D25kkjMk16PbWoYzpSdA8bYpVfS6hpl2a2pOWZ3c+Tyt3/i4/pglyZctG3J4V1hWQ== + dependencies: + "@hapi/joi" "^15.0.3" + core-js "^2.6.5" + minimist "^1.2.0" + request "^2.88.0" + rx "^4.1.0" + +wait-port@^0.2.7: + version "0.2.7" + resolved "https://registry.yarnpkg.com/wait-port/-/wait-port-0.2.7.tgz#cdb4b78e662328099b187c7bb75fe0aa9cb6eb6c" + integrity sha512-pJ6cSBIa0w1sDg4y/wXN4bmvhM9OneOvwdFHo647L2NShBi/oXG4lRaLic5cO1HaYGbUhEvratPfl/WMlIC+tg== + dependencies: + chalk "^2.4.2" + commander "^3.0.2" + debug "^4.1.1" + walker@^1.0.7, walker@~1.0.5: version "1.0.7" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" @@ -20115,10 +20305,10 @@ webpack-virtual-modules@^0.2.0: dependencies: inquirer "^7.0.0" -webpack@^4.33.0, webpack@^4.38.0, webpack@^4.41.6: - version "4.41.6" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.41.6.tgz#12f2f804bf6542ef166755050d4afbc8f66ba7e1" - integrity sha512-yxXfV0Zv9WMGRD+QexkZzmGIh54bsvEs+9aRWxnN8erLWEOehAKUTeNBoUbA6HPEZPlRo7KDi2ZcNveoZgK9MA== +webpack@^4.33.0, webpack@^4.38.0, webpack@^4.42.0: + version "4.42.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.42.0.tgz#b901635dd6179391d90740a63c93f76f39883eb8" + integrity sha512-EzJRHvwQyBiYrYqhyjW9AqM90dE4+s1/XtCfn7uWg6cS72zH+2VPFAlsnW0+W0cDi0XRjNKUMoJtpSi50+Ph6w== dependencies: "@webassemblyjs/ast" "1.8.5" "@webassemblyjs/helper-module-context" "1.8.5" @@ -20230,7 +20420,7 @@ which@1.2.4: is-absolute "^0.1.7" isexe "^1.1.1" -which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1: +which@^1.2.12, which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==