From 341be350bc70b1050387694e08a26c0855eaeda4 Mon Sep 17 00:00:00 2001 From: Mateo Nunez Date: Fri, 10 Feb 2023 18:16:10 +0100 Subject: [PATCH] Add browser support (#48) * feat(test): add browser test system * test(base): add suite on browser tests * refactor: rename browser tests * test(cache): add suite on browser tests * test(clear): add suite on browser tests * test(stale): add suite on browser tests * test(storage-base): add suite on browser tests * test(storage-memory): add suite on browser tests * test(ttl): add suite on browser tests * chore(storage): throw when runs in browser and redis * chore(memory): prevent undefined unref method * feat: add firefox support * feat: add safari support * refactor(abstract-logging): remove dependency and create factory * feat: add rollup bundler support * feat: add webpack bundler support * feat: add browserify bundler support * fix(bench): change createStorage path * chore(package): rename test browser script * feat(ci): add browser ci * chore(readme): add browser documentation * chore: reuse sleep helper method * perf(ci): cache playwright deps * perf(bundlers): dont include redis on browser bundles * feat(bundler): add vite support * chore(test): skip timer tests * chore(test): remove skipped tests * chore(readme): update browser specs * chore(util): comment abstractLogging method * chore(storage): remove istanbul and add test * chore(test-browser): remove duplicated tests * test(storage): add more specific tests * refactor: remove prepare command * ci(browser): remove prepare command --- .github/workflows/browsers-ci.yml | 59 +++++ .gitignore | 3 + README.md | 34 +++ bench/storage.js | 2 +- package.json | 24 ++- src/storage/index.js | 11 +- src/storage/memory.js | 8 +- src/storage/redis.js | 5 +- src/util.js | 21 +- .../fixtures/esbuild.browser-shims.mjs | 7 + .../fixtures/esbuild.browser.config.mjs | 26 +++ test/browser/fixtures/index.html | 72 +++++++ .../fixtures/rollup.browser.config.mjs | 27 +++ test/browser/fixtures/vite.browser.config.mjs | 44 ++++ .../fixtures/webpack.browser.config.mjs | 35 +++ test/browser/helpers/runner-browser.mjs | 204 ++++++++++++++++++ test/browser/helpers/supported-browsers.js | 5 + test/browser/helpers/supported-bundlers.js | 5 + test/browser/helpers/symbols.js | 6 + test/browser/helpers/util.js | 30 +++ test/browser/storage-base.browser.test.js | 17 ++ test/browser/storage-memory.browser.test.js | 29 +++ test/browser/test-browser.js | 84 ++++++++ test/storage-redis.test.js | 10 + 24 files changed, 756 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/browsers-ci.yml create mode 100644 test/browser/fixtures/esbuild.browser-shims.mjs create mode 100644 test/browser/fixtures/esbuild.browser.config.mjs create mode 100644 test/browser/fixtures/index.html create mode 100644 test/browser/fixtures/rollup.browser.config.mjs create mode 100644 test/browser/fixtures/vite.browser.config.mjs create mode 100644 test/browser/fixtures/webpack.browser.config.mjs create mode 100644 test/browser/helpers/runner-browser.mjs create mode 100644 test/browser/helpers/supported-browsers.js create mode 100644 test/browser/helpers/supported-bundlers.js create mode 100644 test/browser/helpers/symbols.js create mode 100644 test/browser/helpers/util.js create mode 100644 test/browser/storage-base.browser.test.js create mode 100644 test/browser/storage-memory.browser.test.js create mode 100644 test/browser/test-browser.js diff --git a/.github/workflows/browsers-ci.yml b/.github/workflows/browsers-ci.yml new file mode 100644 index 0000000..9736660 --- /dev/null +++ b/.github/workflows/browsers-ci.yml @@ -0,0 +1,59 @@ +name: browsers ci + +on: + push: + paths-ignore: + - 'docs/**' + - '*.md' + pull_request: + paths-ignore: + - 'docs/**' + - '*.md' + +jobs: + build: + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: ['ubuntu-latest', 'windows-latest', 'macos-latest'] + browser: ['chrome', 'firefox', 'safari', 'edge'] + bundler: ['browserify', 'esbuild', 'rollup', 'vite', 'webpack'] + exclude: + - os: ubuntu-latest + browser: safari + - os: windows-latest + browser: safari + steps: + - uses: actions/checkout@v3 + + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: Restore cached playwright dependency + uses: actions/cache@v3 + id: playwright-cache + with: + path: | # playwright installs browsers into .local-browsers + ~/.cache/ms-playwright + ~/Library/Caches/ms-playwright + %LOCALAPPDATA%\ms-playwright + key: ${{ matrix.os }}-playwright-${{ hashFiles('package.json') }} + + - name: Restore cached dependencies + uses: actions/cache@v3 + with: + path: node_modules + key: node-modules-${{ matrix.os }}-${{ hashFiles('package.json') }} + + - name: Install dependencies + run: npm install + + - name: Install browser + run: ./node_modules/.bin/playwright install ${{ fromJSON('{"chrome":"chromium","edge":"msedge","firefox":"firefox","safari":"webkit"}')[matrix.browser] }} + + - name: Run Tests on Browsers + run: npm run test:browser ${{ matrix.browser }} ${{ matrix.bundler }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 996fa5c..7fcb1d6 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,6 @@ dist .vscode docker-compose.yml dump.rdb + +# Temporary files used for browser testing +tmp/ \ No newline at end of file diff --git a/README.md b/README.md index b1aee4e..5a28572 100644 --- a/README.md +++ b/README.md @@ -311,6 +311,40 @@ const p1 = cache.fetchSomething(42) // <--- TypeScript doesn't argue anymore her --- +## Browser + +All the major browser are supported; only `memory` storage type is supported, `redis` storage can't be used in a browser env. + +This is a very simple example of how to use this module in a browser environment: + +```html + + + +``` + +You can also use the module with a bundler. The supported bundlers are `webpack`, `rollup`, `esbuild` and `browserify`. + +--- + ## Maintainers * [__Matteo Collina__](https://github.com/mcollina), , diff --git a/bench/storage.js b/bench/storage.js index d406567..74dfa20 100644 --- a/bench/storage.js +++ b/bench/storage.js @@ -3,7 +3,7 @@ const { hrtime } = require('process') const path = require('path') const Redis = require('ioredis') -const createStorage = require(path.resolve(__dirname, '../storage')) +const createStorage = require(path.resolve(__dirname, '../src/storage/index.js')) // NOTE: this is a very basic benchmarks for tweaking // performance is effected by keys and references size diff --git a/package.json b/package.json index 9c372b8..4d3df1e 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "types": "index.d.ts", "scripts": { "test": "standard | snazzy && tap test/*test.js && tsd", + "test:browser": "node test/browser/helpers/runner-browser.mjs", "lint:fix": "standard --fix", "redis": "docker run --rm -p 6379:6379 redis redis-server" }, @@ -30,15 +31,34 @@ "license": "MIT", "devDependencies": { "@fastify/pre-commit": "^2.0.2", + "@rollup/plugin-commonjs": "^24.0.1", + "@rollup/plugin-inject": "^5.0.3", + "@rollup/plugin-node-resolve": "^15.0.1", + "browserify": "^17.0.0", + "buffer": "^6.0.3", + "esbuild": "^0.17.4", + "esbuild-plugin-alias": "^0.2.1", + "events": "^3.3.0", "ioredis": "^5.2.3", + "path-browserify": "^1.0.1", + "playwright": "^1.29.2", + "process": "^0.11.10", "proxyquire": "^2.1.3", + "rollup": "^3.11.0", + "rollup-plugin-polyfill-node": "^0.12.0", "snazzy": "^9.0.0", "standard": "^17.0.0", + "stream-browserify": "^3.0.0", "tap": "^16.3.0", - "tsd": "^0.25.0" + "tap-mocha-reporter": "^5.0.3", + "tap-parser": "^12.0.1", + "tape": "^5.6.3", + "tsd": "^0.25.0", + "vite": "^4.0.4", + "webpack": "^5.75.0", + "webpack-cli": "^5.0.1" }, "dependencies": { - "abstract-logging": "^2.0.1", "mnemonist": "^0.39.2", "safe-stable-stringify": "^2.3.1" }, diff --git a/src/storage/index.js b/src/storage/index.js index 410adf6..c1f0b0b 100644 --- a/src/storage/index.js +++ b/src/storage/index.js @@ -1,6 +1,11 @@ 'use strict' -const StorageRedis = require('./redis') +const { isServerSide } = require('../util') + +let StorageRedis +if (isServerSide) { + StorageRedis = require('./redis') +} const StorageMemory = require('./memory') /** @@ -27,6 +32,10 @@ const StorageOptionsType = { * @returns {StorageMemory|StorageRedis} */ function createStorage (type, options) { + if (!isServerSide && type === StorageOptionsType.redis) { + throw new Error('Redis storage is not supported in the browser') + } + if (type === StorageOptionsType.redis) { return new StorageRedis(options) } diff --git a/src/storage/memory.js b/src/storage/memory.js index 7c98ec2..e4be749 100644 --- a/src/storage/memory.js +++ b/src/storage/memory.js @@ -1,7 +1,7 @@ 'use strict' const LRUCache = require('mnemonist/lru-cache') -const nullLogger = require('abstract-logging') +const { abstractLogging } = require('../util') const StorageInterface = require('./interface') const { findMatchingIndexes, findNotMatching, bsearchIndex, wildcardMatch } = require('../util') @@ -26,7 +26,7 @@ class StorageMemory extends StorageInterface { super(options) this.size = options.size || DEFAULT_CACHE_SIZE - this.log = options.log || nullLogger + this.log = options.log || abstractLogging() this.invalidation = options.invalidation || false this.init() @@ -401,7 +401,9 @@ function now () { return _timer } _timer = Math.floor(Date.now() / 1000) - setTimeout(_clearTimer, 1000).unref() + const timeout = setTimeout(_clearTimer, 1000) + // istanbul ignore next + if (typeof timeout.unref === 'function') timeout.unref() return _timer } diff --git a/src/storage/redis.js b/src/storage/redis.js index 86f9a3e..ec90e14 100644 --- a/src/storage/redis.js +++ b/src/storage/redis.js @@ -1,9 +1,8 @@ 'use strict' const stringify = require('safe-stable-stringify') -const nullLogger = require('abstract-logging') const StorageInterface = require('./interface') -const { findNotMatching, randomSubset } = require('../util') +const { findNotMatching, randomSubset, abstractLogging } = require('../util') const GC_DEFAULT_CHUNK = 64 const GC_DEFAULT_LAZY_CHUNK = 64 @@ -33,7 +32,7 @@ class StorageRedis extends StorageInterface { throw new Error('invalidation.referencesTTL must be a positive integer greater than 1') } - this.log = options.log || nullLogger + this.log = options.log || abstractLogging() this.store = options.client this.invalidation = !!options.invalidation this.referencesTTL = (options.invalidation && options.invalidation.referencesTTL) || REFERENCES_DEFAULT_TTL diff --git a/src/util.js b/src/util.js index a3622e6..33856b0 100644 --- a/src/util.js +++ b/src/util.js @@ -127,11 +127,28 @@ function wildcardMatch (value, content) { return i >= value.length - 1 } +// `abstract-logging` dependency has been removed because there is a bug on Rollup +// https://github.com/jsumners/abstract-logging/issues/6 +function abstractLogging () { + const noop = () => {} + return { + fatal: noop, + error: noop, + warn: noop, + info: noop, + debug: noop, + trace: noop + } +} + +const isServerSide = typeof window === 'undefined' + module.exports = { findNotMatching, findMatchingIndexes, bsearchIndex, wildcardMatch, - - randomSubset + randomSubset, + abstractLogging, + isServerSide } diff --git a/test/browser/fixtures/esbuild.browser-shims.mjs b/test/browser/fixtures/esbuild.browser-shims.mjs new file mode 100644 index 0000000..f8e5173 --- /dev/null +++ b/test/browser/fixtures/esbuild.browser-shims.mjs @@ -0,0 +1,7 @@ +import * as processModule from 'process' + +export const process = processModule + +export function setImmediate (fn, ...args) { + setTimeout(() => fn(...args), 0) +} diff --git a/test/browser/fixtures/esbuild.browser.config.mjs b/test/browser/fixtures/esbuild.browser.config.mjs new file mode 100644 index 0000000..120878c --- /dev/null +++ b/test/browser/fixtures/esbuild.browser.config.mjs @@ -0,0 +1,26 @@ +import { build } from 'esbuild' +import alias from 'esbuild-plugin-alias' +import { createRequire } from 'module' + +const require = createRequire(import.meta.url) + +build({ + entryPoints: ['test/browser/test-browser.js'], + outfile: 'tmp/esbuild/suite.browser.js', + bundle: true, + platform: 'browser', + plugins: [ + alias({ + path: require.resolve('path-browserify'), + stream: require.resolve('stream-browserify') + }) + ], + define: { + global: 'globalThis' + }, + inject: ['test/browser/fixtures/esbuild.browser-shims.mjs'], + external: ['./src/storage/redis.js'] +}).catch((err) => { + console.log(err) + process.exit(1) +}) diff --git a/test/browser/fixtures/index.html b/test/browser/fixtures/index.html new file mode 100644 index 0000000..349557b --- /dev/null +++ b/test/browser/fixtures/index.html @@ -0,0 +1,72 @@ + + + + + + +
+ + + + + diff --git a/test/browser/fixtures/rollup.browser.config.mjs b/test/browser/fixtures/rollup.browser.config.mjs new file mode 100644 index 0000000..ebf8bd9 --- /dev/null +++ b/test/browser/fixtures/rollup.browser.config.mjs @@ -0,0 +1,27 @@ +import commonjs from '@rollup/plugin-commonjs' +import inject from '@rollup/plugin-inject' +import nodeResolve from '@rollup/plugin-node-resolve' +import { resolve } from 'path' +import nodePolyfill from 'rollup-plugin-polyfill-node' + +export default { + input: ['test/browser/test-browser.js'], + external: ['./src/storage/redis.js'], + output: { + intro: 'function setImmediate(fn, ...args) { setTimeout(() => fn(...args), 0) }', + file: 'tmp/rollup/suite.browser.js', + format: 'iife', + name: 'asyncDedupeStorageTestSuite' + }, + plugins: [ + commonjs(), + nodePolyfill(), + inject({ + process: resolve('node_modules/process/browser.js') + }), + nodeResolve({ + browser: true, + preferBuiltins: false + }) + ] +} diff --git a/test/browser/fixtures/vite.browser.config.mjs b/test/browser/fixtures/vite.browser.config.mjs new file mode 100644 index 0000000..37c86ea --- /dev/null +++ b/test/browser/fixtures/vite.browser.config.mjs @@ -0,0 +1,44 @@ +import { defineConfig } from 'vite' +import inject from '@rollup/plugin-inject' +import { resolve } from 'path' +import commonjs from '@rollup/plugin-commonjs' +import { createRequire } from 'module' + +const require = createRequire(import.meta.url) + +export default defineConfig({ + build: { + outDir: 'tmp/vite', + lib: { + entry: 'test/browser/test-browser.js', + name: 'suite', + fileName: () => 'suite.browser.js', + formats: ['iife'] + }, + rollupOptions: { + output: { + intro: 'function setImmediate(fn, ...args) { setTimeout(() => fn(...args), 0) }' + }, + external: ['./src/storage/redis.js'] + }, + emptyOutDir: false, + commonjsOptions: { + include: [/src/], + transformMixedEsModules: true + } + }, + resolve: { + alias: { + path: require.resolve('path-browserify'), + stream: require.resolve('stream-browserify') + } + }, + plugins: [ + commonjs({ + transformMixedEsModules: true + }), + inject({ + process: resolve('node_modules/process/browser.js') + }) + ] +}) diff --git a/test/browser/fixtures/webpack.browser.config.mjs b/test/browser/fixtures/webpack.browser.config.mjs new file mode 100644 index 0000000..440582b --- /dev/null +++ b/test/browser/fixtures/webpack.browser.config.mjs @@ -0,0 +1,35 @@ +import { createRequire } from 'module' +import { resolve } from 'path' +import { fileURLToPath } from 'url' +import webpack from 'webpack' + +const require = createRequire(import.meta.url) +const rootDir = resolve(fileURLToPath(new URL('.', import.meta.url)), '../../../') + +export default { + entry: './test/browser/test-browser.js', + output: { + filename: 'suite.browser.js', + path: resolve(rootDir, 'tmp/webpack') + }, + externals: ['./src/storage/redis.js'], + mode: 'production', + target: 'web', + performance: false, + plugins: [ + new webpack.BannerPlugin({ + banner: 'function setImmediate(fn, ...args) { setTimeout(() => fn(...args), 0) }', + raw: true + }), + new webpack.ProvidePlugin({ + process: require.resolve('process') + }) + ], + resolve: { + aliasFields: ['browser'], + fallback: { + path: require.resolve('path-browserify'), + stream: require.resolve('stream-browserify') + } + } +} diff --git a/test/browser/helpers/runner-browser.mjs b/test/browser/helpers/runner-browser.mjs new file mode 100644 index 0000000..7c4c12a --- /dev/null +++ b/test/browser/helpers/runner-browser.mjs @@ -0,0 +1,204 @@ +import { supportedBrowsers } from './supported-browsers.js' +import { supportedBundlers } from './supported-bundlers.js' +import { chromium, firefox, webkit } from 'playwright' +import { resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { Readable } from 'node:stream' +import { copyFile, mkdir, rm } from 'node:fs/promises' +import { exec } from 'node:child_process' +import { info, error, highlightFile, createDeferredPromise } from './util.js' +import Parser from 'tap-parser' +import Reporter from 'tap-mocha-reporter' + +async function runCommand (command) { + info(`Executing \x1b[33m${command}\x1b[0m ...`) + const { promise, reject, resolve } = createDeferredPromise() + + let hasOutput = false + function logOutput (chunk) { + if (!hasOutput) { + hasOutput = true + info('') + } + + info(chunk.toString('utf-8').trim().replace(/^/gm, ' ')) + } + + try { + const process = exec(command, { stdio: 'pipe' }, (error) => { + if (error) { + return reject(error) + } + + resolve(error) + }) + + process.stdout.on('data', logOutput) + process.stderr.on('data', logOutput) + + await promise + + if (hasOutput) { + info('') + } + } catch (err) { + if (hasOutput) { + info('') + } + + error(`Command \x1b[33m${command}\x1b[0m failed with exit code ${err.code}.`) + process.exit(1) + } +} + +function createConfiguration () { + let [browser, bundler] = process.argv.slice(2, 4) + if (!browser) browser = process.env.BROWSER + if (!bundler) bundler = process.env.BUNDLER + + if (!supportedBrowsers.includes(browser) || !supportedBundlers.includes(bundler)) { + error(`Usage: npm run test:browser [${supportedBrowsers.join('|')}] [${supportedBundlers.join('|')}]`) + error('You can also use the BROWSER and BUNDLER environment variables.') + process.exit(1) + } + + const headless = process.env.HEADLESS !== 'false' + const reporter = process.env.REPORTER !== 'true' + + return { browser, bundler, headless, reporter } +} + +async function setupTest ({ bundler }) { + const rootDir = resolve(fileURLToPath(new URL('.', import.meta.url)), `../../../tmp/${bundler}`) + const sourceIndex = resolve(fileURLToPath(new URL('.', import.meta.url)), '../../../test/browser/fixtures/index.html') + const targetIndex = resolve(rootDir, 'index.html') + + info(`Emptying directory ${highlightFile(rootDir)} ...`) + try { + await rm(rootDir, { recursive: true }) + } catch (err) { + // noop + } + + await mkdir(rootDir, { recursive: true }) + info(`Copying ${highlightFile(sourceIndex)} to ${highlightFile(targetIndex)} ...`) + await copyFile(sourceIndex, targetIndex) + + switch (bundler) { + case 'browserify': { + await runCommand('browserify test/browser/test-browser.js -o tmp/browserify/suite.browser.js -u ./src/storage/redis.js') + break + } + case 'esbuild': { + await runCommand('node test/browser/fixtures/esbuild.browser.config.mjs') + break + } + case 'rollup': { + await runCommand('rollup -c test/browser/fixtures/rollup.browser.config.mjs') + break + } + case 'vite': { + await runCommand('vite build --config test/browser/fixtures/vite.browser.config.mjs') + break + } + case 'webpack': { + await runCommand('webpack -c test/browser/fixtures/webpack.browser.config.mjs') + break + } + } +} + +function createBrowser ({ browser, headless }) { + switch (browser) { + case 'edge': + return chromium.launch({ headless, channel: 'msedge' }) + case 'firefox': + return firefox.launch({ headless }) + case 'safari': + return webkit.launch({ headless }) + default: + return chromium.launch({ headless }) + } +} + +function setupTape (browser, page, config) { + const output = new Readable({ read () {} }) + const parser = new Parser({ strict: true }) + + output.pipe(parser) + + if (config.reporter) { + output.pipe(Reporter('spec')) + } + + parser.on('line', (line) => { + if (line !== '# async-cache-dedupe-finished\n') { + if (line.startsWith('# not ok')) { + process.exitCode = 1 + } + + if (!config.reporter) { + info(line.replace(/\n$/, '')) + } + + return + } + + output.push(null) + + if (config.headless) { + browser.close() + } + }) + + // Catching console errors + page.on('console', (msg) => { + if (msg.type() === 'error') { + error(`\x1b[31m\x1b[1mconsole.error:\x1b[0m ${msg.text()}\n`) + return + } + + output.push(msg.text() + '\n') + }) + + // Firefox in headless mode is showing an error even if onerror caught it. Disable in that case + if (!config.headless || config.browser !== 'firefox') { + page.on('pageerror', (err) => { + error('\x1b[31m\x1b[1m--- The browser thrown an uncaught error ---\x1b[0m') + error(err) + + if (config.headless) { + error('\x1b[31m\x1b[1m--- Exiting with exit code 1 ---\x1b[0m') + process.exit(1) + } else { + process.exitCode = 1 + } + }) + } +} + +async function main () { + const config = createConfiguration() + + // Generate the bundles + await setupTest(config).catch(err => { + error(err) + }) + + // Creating the browser and configuring the pagew with Tape + const browser = await createBrowser(config) + const page = await browser.newPage() + setupTape(browser, page, config) + + // Run the test suite + const __dirname = fileURLToPath(new URL('.', import.meta.url)) + const url = `file://${resolve(__dirname, `../../../tmp/${config.bundler}/index.html`)}` + await page.goto(url).catch((err) => { + error(err) + }) +} + +await main().catch(err => { + error(err) + process.exit(1) +}) diff --git a/test/browser/helpers/supported-browsers.js b/test/browser/helpers/supported-browsers.js new file mode 100644 index 0000000..a1aa9b5 --- /dev/null +++ b/test/browser/helpers/supported-browsers.js @@ -0,0 +1,5 @@ +'use strict' + +const supportedBrowsers = ['chrome', 'edge', 'firefox', 'safari'] + +module.exports = { supportedBrowsers } diff --git a/test/browser/helpers/supported-bundlers.js b/test/browser/helpers/supported-bundlers.js new file mode 100644 index 0000000..c09fe02 --- /dev/null +++ b/test/browser/helpers/supported-bundlers.js @@ -0,0 +1,5 @@ +'use strict' + +const supportedBundlers = ['browserify', 'esbuild', 'rollup', 'vite', 'webpack'] + +module.exports = { supportedBundlers } diff --git a/test/browser/helpers/symbols.js b/test/browser/helpers/symbols.js new file mode 100644 index 0000000..96ffc2f --- /dev/null +++ b/test/browser/helpers/symbols.js @@ -0,0 +1,6 @@ +'use strict' + +module.exports = { + kAsyncCacheDedupeSuiteName: Symbol('async-cache-dedupe.suiteName'), + kAsyncCacheDedupeSuiteHasMultipleTests: Symbol('async-cache-dedupe.suiteHasMultipleTests') +} diff --git a/test/browser/helpers/util.js b/test/browser/helpers/util.js new file mode 100644 index 0000000..96732ac --- /dev/null +++ b/test/browser/helpers/util.js @@ -0,0 +1,30 @@ +'use strict' + +function createDeferredPromise () { + let _resolve + let _reject + + const promise = new Promise((resolve, reject) => { + _resolve = resolve + _reject = reject + }) + return { + promise, + resolve: _resolve, + reject: _reject + } +} + +function highlightFile (file) { + return `\x1b[33m${file.replace(process.cwd() + '/', '')}\x1b[0m` +} + +function info (message) { + console.info(`\x1b[34m[INFO]\x1b[0m ${message}`) +} + +function error (message) { + console.info(`\x1b[31m[ERROR]\x1b[0m ${message}`) +} + +module.exports = { createDeferredPromise, highlightFile, info, error } diff --git a/test/browser/storage-base.browser.test.js b/test/browser/storage-base.browser.test.js new file mode 100644 index 0000000..407b25d --- /dev/null +++ b/test/browser/storage-base.browser.test.js @@ -0,0 +1,17 @@ +'use strict' + +const createStorage = require('../../src/storage') +const { kAsyncCacheDedupeSuiteName, kAsyncCacheDedupeSuiteHasMultipleTests } = require('./helpers/symbols.js') + +module.exports = async function (test) { + test('should fail when using redis storage in the browser', async (t) => { + t.plan(1) + + t.throws(() => { + createStorage('redis', { client: {} }) + }, { message: 'Redis storage is not supported in the browser' }) + }) +} + +module.exports[kAsyncCacheDedupeSuiteName] = 'storage-base browser suite' +module.exports[kAsyncCacheDedupeSuiteHasMultipleTests] = true diff --git a/test/browser/storage-memory.browser.test.js b/test/browser/storage-memory.browser.test.js new file mode 100644 index 0000000..2265ad1 --- /dev/null +++ b/test/browser/storage-memory.browser.test.js @@ -0,0 +1,29 @@ +'use strict' + +const createStorage = require('../../src/storage') +const { kAsyncCacheDedupeSuiteName, kAsyncCacheDedupeSuiteHasMultipleTests } = require('./helpers/symbols.js') + +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) + +module.exports = async function (test) { + test('should get undefined retrieving an expired value', async (t) => { + const storage = createStorage('memory') + + await storage.set('foo', 'bar', 1) + await sleep(1500) + + t.equal(await storage.get('foo'), undefined) + }) + + test('should return the stored value if not expired', async (t) => { + const storage = createStorage('memory') + + await storage.set('foo', 'bar', 1) + await sleep(500) + + t.equal(await storage.get('foo'), 'bar') + }) +} + +module.exports[kAsyncCacheDedupeSuiteName] = 'storage-memory browser suite' +module.exports[kAsyncCacheDedupeSuiteHasMultipleTests] = true diff --git a/test/browser/test-browser.js b/test/browser/test-browser.js new file mode 100644 index 0000000..3f709f9 --- /dev/null +++ b/test/browser/test-browser.js @@ -0,0 +1,84 @@ +'use strict' + +const logger = globalThis.logger || console.log +const { createDeferredPromise } = require('./helpers/util.js') +const tape = require('tape') +const { kAsyncCacheDedupeSuiteName, kAsyncCacheDedupeSuiteHasMultipleTests } = require('./helpers/symbols.js') + +let totalTests = 0 +let completed = 0 +let failed = 0 + +async function test (rootName, fn) { + // Gather all tests in the file + const tests = {} + function addTests (name, fn) { + tests[`${rootName} - ${name}`] = fn + } + if (fn[kAsyncCacheDedupeSuiteHasMultipleTests]) { + fn(addTests) + } else { + tests[rootName] = fn + } + + // Execute each test in a separate harness and then output overall results + for (const [name, subtest] of Object.entries(tests)) { + const currentIndex = ++totalTests + const harness = tape.createHarness() + const { promise, resolve } = createDeferredPromise() + const messages = [`# Subtest: ${name}`] + + harness.createStream().on('data', function (row) { + if (row.startsWith('TAP version') || row.match(new RegExp(`^# (?:${name})`))) { + return + } + messages.push(row.trim().replace(/^/gm, ' ')) + }) + + harness.onFinish(() => { + const success = harness._exitCode === 0 + messages.push(`${success ? 'ok' : 'not ok'} ${currentIndex} - ${name}`) + logger(messages.join('\n')) + completed++ + if (!success) { + failed++ + } + resolve() + }) + + harness(name, subtest) + await promise + } +} + +async function runTests (suites) { + // Setup an interval + const interval = setInterval(() => { + if (completed < totalTests) { + return + } + clearInterval(interval) + + logger(`1..${totalTests}`) + logger(`# tests ${totalTests}`) + logger(`# pass ${completed - failed}`) + logger(`# fail ${failed}`) + logger(`# ${failed === 0 ? 'ok' : 'not ok'}`) + + // This line is used by the playwright script to detect we're done + logger('# async-cache-dedupe-finished') + }, 100) + + // Execute each test serially, to avoid side-effects errors when dealing with global error handling + for (const suite of suites) { + await test(suite[kAsyncCacheDedupeSuiteName], suite) + } +} + +runTests([ + require('./storage-base.browser.test.js'), + require('./storage-memory.browser.test.js') +]).catch((err) => { + console.error(err) + process.exit(1) +}) diff --git a/test/storage-redis.test.js b/test/storage-redis.test.js index 8e95409..0a5531c 100644 --- a/test/storage-redis.test.js +++ b/test/storage-redis.test.js @@ -1017,4 +1017,14 @@ test('storage redis', async (t) => { t.equal(await storage.getTTL('foo'), 0) }) }) + + test('should throw if is not server side and storage is redis', async (t) => { + const createStorageMock = t.mock('../src/storage/index.js', { + '../src/util.js': module.exports = { + isServerSide: false + } + }) + + t.throws(() => createStorageMock('redis', { client: redisClient }), 'Redis storage is not supported in the browser') + }) })