feat(build): TypeScript

BREAKING CHANGE: fireEvent no longer causes a digest. The behavior is
inconsistent with what the browser actually does

BREAKING CHANGE: only a CommonJS distribution is built.

TypeScript used for build, tests, and eslint parser
ts
Jason Staten 4 years ago
parent 4d4020f6f4
commit e4a3bcc9d6

@ -6,7 +6,10 @@ module.exports = {
jest: true,
},
extends: ['eslint:recommended'],
parser: 'babel-eslint',
parser: '@typescript-eslint/parser',
parserOptions: {
sourceType: 'module',
},
rules: {
'linebreak-style': ['error', 'unix'],
'no-console': ['error', {allow: ['warn', 'error']}],
@ -17,5 +20,7 @@ module.exports = {
'object-shorthand': 'error',
'no-useless-rename': 'error',
'no-use-before-define': ['error', {functions: false}],
'no-unused-vars': 'off',
'no-duplicate-imports': 'off',
},
}

1
.gitignore vendored

@ -5,6 +5,7 @@ lib
.opt-out
.DS_Store
.eslintcache
.vscode
yarn-error.log

@ -1,4 +1,5 @@
module.exports = {
presets: ['@babel/preset-typescript'],
env: {
test: {
plugins: [

@ -3,19 +3,18 @@
"version": "0.0.0-semantic-release",
"description": "Simple and complete AngularJS testing utilities that encourage good testing practices.",
"main": "lib/index.js",
"module": "lib/angularjs-testing-library.mjs",
"engines": {
"node": ">=10"
},
"scripts": {
"prebuild": "rimraf lib",
"build": "rollup -c",
"lint": "eslint --cache .",
"build": "tsc --noEmit false",
"lint": "eslint --ext .ts,.js --cache .",
"test": "jest",
"validate": "concurrently yarn:validate:*",
"validate:lnt": "yarn --silent lint",
"validate:bld": "yarn --silent build",
"validate:tst": "yarn --silent test"
"validate:lint": "yarn --silent lint",
"validate:build": "yarn --silent build",
"validate:test": "yarn --silent test"
},
"husky": {
"hooks": {
@ -25,7 +24,12 @@
"lint-staged": {
"*.{js}": [
"prettier --write",
"eslint",
"eslint --fix",
"git add"
],
"*.{ts}": [
"prettier --write",
"eslint --fix",
"git add"
],
"*.{json,css,md}": [
@ -36,13 +40,8 @@
"doctoc --maxlevel 3 --notitle"
]
},
"jest": {
"setupFilesAfterEnv": [
"@testing-library/jest-dom/extend-expect"
]
},
"files": [
"lib",
"lib/*.{js,ts}",
"dont-cleanup-after-each.js"
],
"keywords": [
@ -66,24 +65,26 @@
"@testing-library/dom": "^6.3.0"
},
"devDependencies": {
"@babel/core": "^7.7.5",
"@babel/plugin-proposal-class-properties": "^7.7.4",
"@babel/plugin-transform-modules-commonjs": "^7.7.5",
"@semantic-release/git": "^7.0.17",
"@testing-library/jest-dom": "^4.1.0",
"@types/angular": "^1.6.57",
"@types/angular-mocks": "^1.7.0",
"@types/jest": "^24.0.23",
"@types/node": "^12.12.16",
"@typescript-eslint/parser": "^2.11.0",
"angular": "^1.7.8",
"angular-mocks": "^1.7.8",
"babel-eslint": "^10.0.3",
"babel-jest": "^24.9.0",
"concurrently": "^5.0.0",
"doctoc": "^1.4.0",
"eslint": "^6.7.2",
"husky": "^3.1.0",
"jest": "^24.9.0",
"jest-cli": "^24.9.0",
"lint-staged": "^9.5.0",
"rimraf": "^3.0.0",
"rollup": "^1.27.8",
"semantic-release": "^15.13.30"
"semantic-release": "^15.13.30",
"ts-jest": "^24.2.0",
"typescript": "^3.7.3"
},
"peerDependencies": {
"angular": "*",
@ -117,5 +118,11 @@
]
],
"repositoryUrl": "git@git.sr.ht:~statianzo/angularjs-testing-library"
},
"jest": {
"preset": "ts-jest",
"setupFilesAfterEnv": [
"@testing-library/jest-dom/extend-expect"
]
}
}

@ -1,22 +0,0 @@
import * as pkg from './package.json'
const pkgDependencies = Object.keys({
...pkg.dependencies,
...pkg.devDependencies,
...pkg.peerDependencies,
...pkg.optionalDependencies,
})
export default {
input: 'src/index',
output: [
{
file: pkg.main,
format: 'cjs',
sourcemap: true,
externalLiveBindings: false,
},
{file: pkg.module, format: 'es', sourcemap: true},
],
external: [...pkgDependencies],
}

@ -1,4 +1,4 @@
let render
let render: (html: string) => void
beforeAll(() => {
process.env.ATL_SKIP_AUTO_CLEANUP = 'true'
const atl = require('../')

@ -1,4 +1,4 @@
import angular from 'angular'
import * as angular from 'angular'
import 'angular-mocks'
import {render, cleanup} from '../'

@ -1,4 +1,4 @@
import angular from 'angular'
import * as angular from 'angular'
import 'angular-mocks'
import {render} from '../'
@ -7,7 +7,7 @@ beforeEach(() => {
})
afterEach(() => {
console.log.mockRestore()
(console.log as jest.Mock).mockRestore()
})
beforeEach(() => {

@ -1,39 +0,0 @@
import angular from 'angular'
import 'angular-mocks'
import {render, fireEvent} from '../'
beforeEach(() => {
angular.module('atl', [])
angular.mock.module('atl')
})
test('`fireEvent` triggers a digest', () => {
angular.module('atl').component('atlDigest', {
template: `
<button ng-ref="$ctrl.btn">
Click Me
</button>
<div ng-if="$ctrl.wasClicked">
Clicked!
</div>
`,
controller: class {
wasClicked = false
btn = null
$postLink() {
this.btn.on('click', this.handleClick)
}
handleClick = () => {
this.wasClicked = true
}
},
})
const {getByRole, queryByText} = render(`<atl-digest></atl-digest>`)
const button = getByRole('button')
expect(queryByText('Clicked!')).toBeNull()
fireEvent.click(button)
expect(queryByText('Clicked!')).not.toBeNull()
})

@ -1,17 +1,21 @@
import angular from 'angular'
import * as angular from 'angular'
import 'angular-mocks'
import {render, wait} from '../'
type FakeResponse = {
returnedMessage: string
}
class controller {
loading = true
data = null
data?: FakeResponse
constructor($q, $timeout) {
this.$q = $q
this.$timeout = $timeout
}
constructor(
private $q: angular.IQService,
private $timeout: angular.ITimeoutService,
) {}
load() {
load(): angular.IPromise<FakeResponse> {
return this.$q(resolve => {
// we are using random timeout here to simulate a real-time example
// of an async operation calling a callback at a non-deterministic time

@ -1,6 +1,13 @@
import {render, fireEvent} from '../'
const eventTypes = [
type EventType = {
type: string
events: Array<keyof typeof fireEvent>
elementType: string
init?: object
}
const eventTypes: EventType[] = [
{
type: 'Clipboard',
events: ['copy', 'paste'],
@ -146,7 +153,7 @@ eventTypes.forEach(({type, events, elementType, init}) => {
},
)
fireEvent[eventName](container.firstChild, init)
fireEvent[eventName](container.firstElementChild as Element, init)
expect(spy).toHaveBeenCalledTimes(1)
})
})
@ -174,7 +181,6 @@ test('calling `fireEvent` directly works too', () => {
new Event('click', {
bubbles: true,
cancelable: true,
button: 0,
}),
)
expect(spy).toHaveBeenCalledTimes(1)

@ -1,7 +1,8 @@
import {render} from '../'
// these are created once per test suite and reused for each case
let treeA, treeB
let treeA: HTMLDivElement
let treeB: HTMLDivElement
beforeAll(() => {
treeA = document.createElement('div')
treeB = document.createElement('div')
@ -10,8 +11,8 @@ beforeAll(() => {
})
afterAll(() => {
treeA.parentNode.removeChild(treeA)
treeB.parentNode.removeChild(treeB)
(treeA.parentNode as Node).removeChild(treeA)
;(treeB.parentNode as Node).removeChild(treeB)
})
test('baseElement isolates trees from one another', () => {

@ -1,4 +1,4 @@
import angular from 'angular'
import * as angular from 'angular'
import 'angular-mocks'
import {render} from '../'
@ -9,7 +9,7 @@ beforeEach(() => {
test('renders div into document', () => {
const {container} = render(`<div id="child"></div>`)
expect(container.firstChild.id).toBe('child')
expect((container.firstElementChild as Element).id).toBe('child')
})
test('returns baseElement which defaults to document.body', () => {

@ -1,23 +1,24 @@
import angular from 'angular'
import * as angular from 'angular'
import 'angular-mocks'
import {render, fireEvent, wait} from '../'
class StopWatch {
timer!: angular.IPromise<void>
lapse = 0
running = false
constructor($interval) {
constructor(private $interval: angular.IIntervalService) {
this.$interval = $interval
}
handleRunClick = () => {
if (this.running) {
clearInterval(this.timer)
this.$interval.cancel(this.timer)
} else {
const startTime = Date.now() - this.lapse
this.timer = this.$interval(() => {
this.lapse = Date.now() - startTime
})
}, 0)
}
this.running = !this.running
}
@ -70,5 +71,5 @@ test('unmounts a component', async () => {
unmount()
expect($scope.$$destroyed).toBe(true)
expect(($scope as any).$$destroyed).toBe(true)
})

@ -1,5 +1,7 @@
import {flush, cleanup} from './pure'
declare function afterEach(cb: () => void): void
// if we're running in a test runner that supports afterEach
// then we'll automatically run cleanup afterEach test
// this ensures that tests run in isolation from each other

@ -1,25 +1,57 @@
import angular from 'angular'
import * as angular from 'angular'
import {
ICompileService,
IScope,
IRootScopeService,
IIntervalService,
IFlushPendingTasksService,
} from 'angular'
import 'angular-mocks'
import {
getQueriesForElement,
prettyDOM,
fireEvent as dtlFireEvent,
wait as dtlWait,
queries as dtlQueries,
Queries,
BoundFunction,
} from '@testing-library/dom'
const mountedContainers = new Set()
const mountedScopes = new Set()
const mountedContainers = new Set<HTMLElement>()
const mountedScopes = new Set<IScope>()
type RenderOptions<Q extends Queries = typeof dtlQueries> = {
container?: HTMLElement
baseElement?: HTMLElement
queries?: Q
scope?: object
ignoreUnknownElements?: boolean
}
type RenderResult<Q extends Queries = typeof dtlQueries> = {
container: HTMLElement
baseElement: HTMLElement
debug: (
baseElement?:
| HTMLElement
| DocumentFragment
| Array<HTMLElement | DocumentFragment>,
) => void
unmount: () => void
asFragment: () => DocumentFragment
$scope: IScope
} & {[P in keyof Q]: BoundFunction<Q[P]>}
function render(
ui,
ui: string,
{
container,
baseElement = container,
queries,
scope,
ignoreUnknownElements,
} = {},
) {
ignoreUnknownElements = false,
}: RenderOptions = {},
): RenderResult {
if (!baseElement) {
// default to document.body instead of documentElement to avoid output of potentially-large
// head elements (such as JSS style blocks) in debug output
@ -34,8 +66,8 @@ function render(
// they're passing us a custom container or not.
mountedContainers.add(container)
const $rootScope = inject('$rootScope')
const $compile = inject('$compile')
const $rootScope = inject<IRootScopeService>('$rootScope')
const $compile = inject<ICompileService>('$compile')
const $scope = $rootScope.$new()
Object.assign($scope, scope)
@ -56,19 +88,18 @@ function render(
debug: (el = baseElement) =>
Array.isArray(el)
? // eslint-disable-next-line no-console
el.forEach(e => console.log(prettyDOM(e)))
el.forEach(e => console.log(prettyDOM(e as HTMLElement)))
: // eslint-disable-next-line no-console,
console.log(prettyDOM(el)),
console.log(prettyDOM(el as HTMLElement)),
asFragment: () => {
const source = container as HTMLElement
/* istanbul ignore if (jsdom limitation) */
if (typeof document.createRange === 'function') {
return document
.createRange()
.createContextualFragment(container.innerHTML)
return document.createRange().createContextualFragment(source.innerHTML)
}
const template = document.createElement('template')
template.innerHTML = container.innerHTML
template.innerHTML = source.innerHTML
return template.content
},
$scope,
@ -86,28 +117,28 @@ function cleanup() {
// maybe one day we'll expose this (perhaps even as a utility returned by render).
// but let's wait until someone asks for it.
function cleanupAtContainer(container) {
function cleanupAtContainer(container: HTMLElement) {
if (container.parentNode === document.body) {
document.body.removeChild(container)
}
mountedContainers.delete(container)
}
function cleanupScope(scope) {
function cleanupScope(scope: IScope) {
scope.$destroy()
mountedScopes.delete(scope)
}
function toCamel(s) {
function toCamel(s: string) {
return s
.toLowerCase()
.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
}
function assertNoUnknownElements(element) {
function assertNoUnknownElements(element: Element) {
const {tagName} = element
if (tagName.includes('-')) {
const $injector = inject('$injector')
const $injector = inject<angular.auto.IInjectorService>('$injector')
const directiveName = `${toCamel(tagName)}Directive`
if (!$injector.has(directiveName)) {
throw Error(
@ -118,7 +149,7 @@ function assertNoUnknownElements(element) {
Array.from(element.children).forEach(assertNoUnknownElements)
}
function inject(name) {
function inject<T = unknown>(name: string): T {
let service
angular.mock.inject([
name,
@ -126,39 +157,30 @@ function inject(name) {
service = injected
},
])
return service
}
function fireEvent(...args) {
const $rootScope = inject('$rootScope')
const result = dtlFireEvent(...args)
$rootScope.$digest()
return result
return (service as unknown) as T
}
Object.keys(dtlFireEvent).forEach(key => {
fireEvent[key] = (...args) => {
const $rootScope = inject('$rootScope')
const result = dtlFireEvent[key](...args)
$rootScope.$digest()
return result
}
})
// AngularJS maps `mouseEnter` to `mouseOver` and `mouseLeave` to `mouseOut`
fireEvent.mouseEnter = fireEvent.mouseOver
fireEvent.mouseLeave = fireEvent.mouseOut
// Create a copy so we can alter it
const fireEvent = dtlFireEvent.bind(null)
Object.assign(fireEvent, dtlFireEvent)
fireEvent.mouseEnter = dtlFireEvent.mouseOver
fireEvent.mouseLeave = dtlFireEvent.mouseOut
function flush(millis = 50) {
const $browser = inject('$browser')
const $rootScope = inject('$rootScope')
const $interval = inject('$interval')
const $flushPendingTasks = inject<IFlushPendingTasksService>(
'$flushPendingTasks',
)
const $rootScope = inject<IRootScopeService>('$rootScope')
const $interval = inject<IIntervalService>('$interval')
$interval.flush(millis)
$browser.defer.flush(millis)
$flushPendingTasks(millis)
$rootScope.$digest()
}
function wait(callback, options = {}) {
type WaitOptions = Parameters<typeof dtlWait>[1]
function wait(callback?: () => void, options: WaitOptions = {}) {
return dtlWait(() => {
flush(options.interval)
if (callback) {

@ -1 +0,0 @@
import '@testing-library/jest-dom/extend-expect'

@ -0,0 +1,78 @@
{
"compilerOptions": {
/* Basic Options */
"incremental": true /* Enable incremental compilation */,
"target":
"es2019" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
"module":
"commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
"declaration": true /* Generates corresponding '.d.ts' file. */,
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./lib" /* Redirect output structure to the directory. */,
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
"noEmit": true /* Do not emit outputs. */,
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
"noUnusedLocals": true /* Report errors on unused locals. */,
"noUnusedParameters": true /* Report errors on unused parameters. */,
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
"moduleResolution":
"node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {
// "src/*": ["/dev/null"], // Enforce relative paths
// }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
"types": [
"node",
"jest",
"@testing-library/jest-dom/extend-expect"
] /* Type declaration files to be included in compilation. */,
"allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
// "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
},
"include": ["src/**/*", "src/__tests__"],
"exclude": ["node_modules"]
}
Loading…
Cancel
Save