Skip to content

Commit

Permalink
Add specs
Browse files Browse the repository at this point in the history
mocha + sinon + chai.

ESM + typescript is hard :-/
Editor TS, build TS and spec TS don't want to agree on module imports.
On top of that — Jakefile can not be ESM yet.
So src/ and /spec are modules, and project is not.
--watch won't work unless ran with --parallel

mochajs/mocha#4374
mochajs/mocha-examples#47
https://gist.github.com/jordansexton/2a0c3c360aa700cc9528e89620e82c3d
  • Loading branch information
falsefalse committed Apr 9, 2023
1 parent 91876de commit 6e23725
Show file tree
Hide file tree
Showing 10 changed files with 1,168 additions and 31 deletions.
10 changes: 10 additions & 0 deletions .mocharc.json
@@ -0,0 +1,10 @@
{
"extension": ["ts", "js"],
"spec": ["spec/**/*.spec.ts"],
"require": ["jsdom-global/register", "spec/setup.ts"],
"node-option": [
"no-warnings",
"experimental-specifier-resolution=node",
"loader=ts-node/esm"
]
}
22 changes: 19 additions & 3 deletions package.json
Expand Up @@ -12,25 +12,41 @@
"release:firefox": "yarn -s _stash_lint_clean && jake -q firefox && open ./pkg",
"ch": "DEV=1 jake -q",
"ff": "DEV=1 jake -q firefox",
"tc": "tsc --noEmit",
"tsc": "tsc -p ts.src.json",
"tc": "yarn -s tsc --noEmit",
"lint": "yarn -s format:check & p=$! ; yarn -s eslint ||s=1 ; wait $p||s=1 ; exit $s",
"eslint": "eslint --fix *.js **/*.ts **/*.ejs.html",
"eslint": "eslint --fix *.js src/ spec/ --ext .ts,.ejs.html",
"format:check": "yarn -s _prettier --check . ||s=1 ; yarn -s _restore ; exit $s",
"format": "yarn -s _prettier -w . ; yarn -s _restore",
"_prettier": "p=.prettierignore ; cat $p > .tmp ; cat .gitignore >> $p ; yarn -s prettier -u --loglevel warn",
"_restore": "cat .tmp > .prettierignore ; rm .tmp"
"_restore": "cat .tmp > .prettierignore ; rm .tmp",
"test:watch": "yarn -s test --watch --parallel"
"test": "DEV=1 jake config[specs] ; TS_NODE_PROJECT='./ts.spec.json' mocha",
},
"devDependencies": {
"@types/chai": "^4.3.4",
"@types/chrome": "^0.0.227",
"@types/firefox-webext-browser": "^111.0.1",
"@types/jsdom": "^21.1.1",
"@types/mocha": "^10.0.1",
"@types/node": "^18.15.11",
"@types/offscreencanvas": "^2019.7.0",
"@types/sinon": "^10.0.13",
"@types/sinon-chai": "^3.2.9",
"@typescript-eslint/eslint-plugin": "^5.57.0",
"@typescript-eslint/parser": "^5.57.0",
"chai": "^4.3.7",
"eslint": "^8.32.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-lodash-template": "^0.21.0",
"jsdom": "^21.1.1",
"jsdom-global": "^3.0.2",
"lodash.template": "^4.5.0",
"mocha": "^10.2.0",
"prettier": "^2.8.3",
"sinon": "^15.0.3",
"sinon-chai": "^3.7.0",
"ts-node": "^10.9.1",
"typescript": "^5.0.3",
"uglify-js": "^3.17.4"
}
Expand Down
115 changes: 115 additions & 0 deletions spec/helpers.spec.ts
@@ -0,0 +1,115 @@
import { SinonStub } from 'sinon'
import { expect } from 'chai'

import { isLocal, getDomain, storage } from '../src/helpers.js'

describe('helpers.ts', () => {
it('creates 64x64 canvas and 2d context', () => {
const {
OffscreenCanvas,
OffscreenCanvas: {
prototype: { getContext }
}
} = global

expect(OffscreenCanvas).to.be.calledOnceWith(64, 64)
expect(getContext).to.be.calledOnceWith('2d', {
willReadFrequently: true
})
})

describe('isLocal', () => {
// prettier-ignore
const cases = [
[true, 'localhost'],
[true, '0.0.0.0'],
[false, '0.0.0.1'],

[true, '127.0.0.0'],
[true, '127.0.0.1'],
[true, '127.1.1.1'],

[true, '10.0.0.1'],
[true, '10.1.1.1'],
[true, '10.0.0.0'],

[false, '172.15.0.0'],
[true, '172.16.0.0'],
[true, '172.16.100.0'],
[true, '172.31.0.0'],
[true, '172.31.0.0'],
[false, '172.32.0.0'],

[true, '192.168.0.0'],
[false, '192.169.0.0'],
[false, '191.168.0.0'],

[false, '0.0'],
[false, undefined],
[false, '123'],
[false, '127.0.0.0.boop.com'],
[false, '10.o.0.0.com'],
[false, '8.8.8.8'],
[false, 'geo.furman.im'],
[false, 'battlestation'],
] as const

cases.forEach(([expected, ip]) => {
it(`${expected ? 'local' : 'global'}\t${ip}`, () => {
expect(isLocal(ip)).to.equal(expected)
})
})
})

describe('getDomain', () => {
it('returns domain for http, https and ftp schemes', () => {
expect(getDomain('http://boop.doop')).to.eq('boop.doop')
expect(
getDomain('https://127.0.0.0.boop.com/welp?some=come&utm=sucks')
).to.eq('127.0.0.0.boop.com')
expect(getDomain('ftp://scene')).to.eq('scene')
})

it('returns undefined for everything else', () => {
expect(getDomain('')).to.be.undefined
expect(getDomain(undefined)).to.be.undefined
expect(getDomain('gopher://old')).to.be.undefined
expect(getDomain('chrome://new-tab')).to.be.undefined
expect(getDomain('magnet://h.a.s.h')).to.be.undefined
})
})

describe('storage', () => {
const { set, get, clear } = global.chrome.storage.local
const setStub = set as SinonStub,
getStub = get as SinonStub,
clearStub = clear as SinonStub

it('#set', () => {
storage.set('boop', { woop: 'shmloop' })

expect(setStub).to.be.calledOnceWith({
boop: { woop: 'shmloop' }
})
})

it('#get', async () => {
getStub.resolves({
'a key': 'valooe',
'another key': 'another valooe'
})

expect(await storage.get('but a key')).to.be.undefined
expect(await storage.get('a key')).to.eq('valooe')
expect(await storage.get('another key')).to.eq('another valooe')
})

it('clears itself when full', async () => {
setStub.throws('anything')

await storage.set('smol', 'but fatal')

expect(clearStub).to.be.calledOnce
})
})
})
3 changes: 3 additions & 0 deletions spec/package.json
@@ -0,0 +1,3 @@
{
"type": "module"
}
47 changes: 47 additions & 0 deletions spec/setup.ts
@@ -0,0 +1,47 @@
import sinon from 'sinon'
import chai from 'chai'
import sinonChai from 'sinon-chai'

/* Matchers */

chai.use(sinonChai)

/* OffscreenCanvas */

const canvasSandbox = sinon.createSandbox({
properties: ['spy']
})

class OffscreenCanvasMock {
constructor() {}
getContext() {}
}

canvasSandbox.spy(OffscreenCanvasMock.prototype, 'getContext')

/* chrome.storage.local */

const storageSandbox = sinon.createSandbox({
properties: ['stub']
})

const storageStub = {
set: storageSandbox.stub(),
get: storageSandbox.stub(),
clear: storageSandbox.stub()
}

/* Assign to window */
Object.assign(global, {
OffscreenCanvas: canvasSandbox.spy(OffscreenCanvasMock),
chrome: { storage: { local: storageStub } }
})

/* Reset call counts hook */

export const mochaHooks = {
afterEach() {
canvasSandbox.reset()
storageSandbox.reset()
}
}
3 changes: 3 additions & 0 deletions src/package.json
@@ -0,0 +1,3 @@
{
"type": "module"
}
9 changes: 9 additions & 0 deletions ts.spec.json
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",

"include": ["spec/**/*"],

"compilerOptions": {
"noEmit": true
}
}
9 changes: 9 additions & 0 deletions ts.src.json
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",

"include": ["src/**/*"],

"compilerOptions": {
"outDir": "build/"
}
}
15 changes: 6 additions & 9 deletions tsconfig.json
@@ -1,20 +1,17 @@
{
"include": ["src/**/*"],

"compilerOptions": {
"rootDir": "src",
"outDir": "build",

"noErrorTruncation": true,
"noErrorTruncation": true, // no (...) in errors

/* typechecking */
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
// do not skipLibCheck, we want our d.ts validated

// modules
/* modules */
"moduleResolution": "nodenext", // this makes other peoples d.ts pass
"esModuleInterop": true,
"module": "es2022",
"target": "es2021",
"esModuleInterop": true
"target": "es2021"
}
}

0 comments on commit 6e23725

Please sign in to comment.