From 862822762c1b21486838b1ad603df522e16e730c Mon Sep 17 00:00:00 2001 From: D-Sketon <2055272094@qq.com> Date: Mon, 15 Jul 2024 21:54:08 +0800 Subject: [PATCH 1/3] test: improve coverage --- .gitignore | 2 - README.md | 2 +- package.json | 17 +- src/anime/Anime.ts | 4 +- src/anime/lib/engine.ts | 29 +-- src/anime/lib/penner.ts | 5 +- src/entity/Circle.ts | 1 + src/entity/Star.ts | 3 +- src/factory.ts | 135 +++++------- src/index.ts | 102 ++++----- src/utils.ts | 16 +- test/index.ts | 471 ++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 1 + 13 files changed, 612 insertions(+), 176 deletions(-) create mode 100644 test/index.ts diff --git a/.gitignore b/.gitignore index ffff0f3..becdf8d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,3 @@ coverage/ /dist pnpm-lock.yaml - -/test \ No newline at end of file diff --git a/README.md b/README.md index 8427bc3..6c19adf 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ e.g. particles: [ { shape: "circle", - move: "emit", + move: ["emit"], easing: "easeOutExpo", colors: [ "rgba(255,182,185,.9)", diff --git a/package.json b/package.json index 23e017c..59bf1a9 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,9 @@ "types": "dist/index.d.ts", "scripts": { "build": "rollup -c", - "build:umd": "rollup -c umd.config.mjs" + "build:umd": "rollup -c umd.config.mjs", + "test": "mocha -r ts-node/register 'test/**/*.ts'", + "test-cov": "c8 --reporter=lcov --reporter=text-summary npm test" }, "author": "D-Sketon", "license": "MIT", @@ -17,12 +19,23 @@ "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^11.1.5", + "@types/chai": "^4.3.16", + "@types/jsdom": "^21.1.7", + "@types/mocha": "^10.0.7", "@types/node": "^20.10.6", + "@types/sinon": "^17.0.3", + "c8": "^10.1.2", + "chai": "^4.4.1", + "jsdom": "^24.1.0", + "mocha": "^10.6.0", "rollup": "^4.9.2", + "sinon": "^18.0.0", + "ts-node": "^10.9.2", + "tslib": "^2.6.3", "typescript": "^5.3.3" }, "dependencies": { - "theme-shokax-anime": "^0.0.6" + "theme-shokax-anime": "^0.0.7" }, "repository": { "type": "git", diff --git a/src/anime/Anime.ts b/src/anime/Anime.ts index e703fbb..97473cf 100644 --- a/src/anime/Anime.ts +++ b/src/anime/Anime.ts @@ -24,7 +24,7 @@ export default class Anime { tl: Timeline; isPlay: boolean; constructor(options: AnimeOptions = defaultOptions) { - options = Object.assign({}, defaultOptions, options); + options = { ...defaultOptions, ...options }; const { targets, duration, @@ -42,7 +42,7 @@ export default class Anime { } timeline() { - if (this.tl === null) { + if (!this.tl) { this.tl = new Timeline(); } return this.tl; diff --git a/src/anime/lib/engine.ts b/src/anime/lib/engine.ts index 5bd2b8f..c6b3d5a 100644 --- a/src/anime/lib/engine.ts +++ b/src/anime/lib/engine.ts @@ -52,11 +52,9 @@ export default (anime: Anime) => { if (!Array.isArray(dest)) { // 不支持keyframe模式 // 支持nest模式 {value: 1, duration: 500, easing: 'linear'} - const { value, duration, easing } = dest; + const { value, duration, easing = anime.easing } = dest; if (current <= start + duration) { - elapsed = penner()[easing ? easing : anime.easing]()( - (current - start) / duration - ); + elapsed = penner()[easing]()((current - start) / duration); change(target, origin, elapsed, value, key); } else if (final) { change(target, origin, elapsed, value, key, final); @@ -82,21 +80,18 @@ export default (anime: Anime) => { // 数据回正 changeAll(1, current, true); anime.isPlay = false; - return; - } - // 还未开始,继续delay - if (current < start) { + } else { + if (current >= start) { + const elapsed = penner()[anime.easing]()( + (current - start) / anime.duration + ); + isValid && changeAll(elapsed, current); + // 调用更新回调 + typeof anime.update == "function" && + anime.update(anime.targets as object[]); + } requestAnimationFrame(step); - return; } - const elapsed = penner()[anime.easing]()( - (current - start) / anime.duration - ); - isValid && changeAll(elapsed, current); - // 调用更新回调 - typeof anime.update == "function" && - anime.update(anime.targets as object[]); - requestAnimationFrame(step); }; initTarget(); diff --git a/src/anime/lib/penner.ts b/src/anime/lib/penner.ts index 36a5659..d27bdd7 100644 --- a/src/anime/lib/penner.ts +++ b/src/anime/lib/penner.ts @@ -8,6 +8,7 @@ export default (): EasingFunctions => { const functionEasings = { Sine: () => (t: number) => 1 - Math.cos((t * Math.PI) / 2), + Expo: () => (t: number) => (t ? Math.pow(2, 10 * t - 10) : 0), Circ: () => (t: number) => 1 - Math.sqrt(1 - t * t), Back: () => (t: number) => t * t * (3 * t - 2), Bounce: () => (t: number) => { @@ -20,9 +21,7 @@ export default (): EasingFunctions => { }, }; - const baseEasings = ["Quad", "Cubic", "Quart", "Quint", "Expo"]; - - baseEasings.forEach((name, i) => { + ["Quad", "Cubic", "Quart", "Quint"].forEach((name, i) => { functionEasings[name] = () => (t: number) => Math.pow(t, i + 2); }); diff --git a/src/entity/Circle.ts b/src/entity/Circle.ts index 0e71b35..f48c2a7 100644 --- a/src/entity/Circle.ts +++ b/src/entity/Circle.ts @@ -4,5 +4,6 @@ export default class Circle extends BaseEntity { paint(): void { this.ctx.beginPath(); this.ctx.arc(0, 0, this.radius, 0, 2 * Math.PI); + this.ctx.closePath(); } } diff --git a/src/entity/Star.ts b/src/entity/Star.ts index a619756..4e2d65d 100644 --- a/src/entity/Star.ts +++ b/src/entity/Star.ts @@ -19,7 +19,7 @@ export default class Star extends BaseEntity { paint(): void { const { ctx, spikes, radius } = this; ctx.beginPath(); - ctx.moveTo(0, 0 - radius); + ctx.moveTo(0, -radius); for (let i = 0; i < spikes * 2; i++) { const angle = (i * Math.PI) / spikes - Math.PI / 2; const length = i % 2 === 0 ? radius : radius * 0.5; @@ -30,3 +30,4 @@ export default class Star extends BaseEntity { ctx.closePath(); } } + \ No newline at end of file diff --git a/src/factory.ts b/src/factory.ts index a51f2c7..a0b072d 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -1,50 +1,65 @@ import anime from "theme-shokax-anime"; // import anime from "./anime"; +import BaseEntity from "./entity/BaseEntity"; import Circle from "./entity/Circle"; import Polygon from "./entity/Polygon"; import Star from "./entity/Star"; -import { - ParticleOptions, - CircleOptions, - StarOptions, - PolygonOptions, -} from "./types"; -import { sample, setEndPos, setEndRotation } from "./utils"; +import { ParticleOptions, StarOptions, PolygonOptions } from "./types"; +import { formatAlpha, sample, setEndPos, setEndRotation } from "./utils"; -export const createCircle = ( +const preProcess = ( ctx: CanvasRenderingContext2D, x: number, y: number, - particle: ParticleOptions -): Circle[] => { + particle: ParticleOptions, + shapeType: typeof Circle | typeof Polygon | typeof Star +) => { const num = sample(particle.number); - let { - radius, - alpha = 100, - lineWidth, - } = particle.shapeOptions as CircleOptions; - if (Array.isArray(alpha)) { - alpha = alpha.map((a) => a * 100) as [number, number]; - } else { - alpha *= 100; - } - const circles = []; + let { radius, alpha = 1, lineWidth } = particle.shapeOptions; + alpha = formatAlpha(alpha); + const shapes: BaseEntity[] = []; for (let i = 0; i < num; i++) { - const p = new Circle( - ctx, - x, - y, - particle.colors[anime.random(0, particle.colors.length - 1)], - sample(radius), - sample(alpha) / 100, - sample(lineWidth) - ); + const color = particle.colors[anime.random(0, particle.colors.length - 1)]; + let p: BaseEntity; + if (shapeType === Circle) { + p = new shapeType( + ctx, + x, + y, + color, + sample(radius), + sample(alpha) / 100, + sample(lineWidth) + ); + } else { + p = new shapeType( + ctx, + x, + y, + color, + sample(radius), + sample(alpha) / 100, + shapeType === Star + ? sample((particle.shapeOptions as StarOptions).spikes) + : sample((particle.shapeOptions as PolygonOptions).sides), + sample(lineWidth) + ); + } setEndPos(p, particle); setEndRotation(p, particle); - circles.push(p); + shapes.push(p); } - return circles; + return shapes; +}; + +export const createCircle = ( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + particle: ParticleOptions +): Circle[] => { + return preProcess(ctx, x, y, particle, Circle) as Circle[]; }; export const createStar = ( @@ -53,31 +68,7 @@ export const createStar = ( y: number, particle: ParticleOptions ): Star[] => { - const num = sample(particle.number); - let { radius, alpha = 100, lineWidth } = particle.shapeOptions as StarOptions; - if (Array.isArray(alpha)) { - alpha = alpha.map((a) => a * 100) as [number, number]; - } else { - alpha *= 100; - } - const spikes = sample((particle.shapeOptions as StarOptions).spikes); - const stars = []; - for (let i = 0; i < num; i++) { - const p = new Star( - ctx, - x, - y, - particle.colors[anime.random(0, particle.colors.length - 1)], - sample(radius), - sample(alpha) / 100, - spikes, - sample(lineWidth) - ); - setEndPos(p, particle); - setEndRotation(p, particle); - stars.push(p); - } - return stars; + return preProcess(ctx, x, y, particle, Star) as Star[]; }; export const createPolygon = ( @@ -86,33 +77,5 @@ export const createPolygon = ( y: number, particle: ParticleOptions ): Polygon[] => { - const num = sample(particle.number); - let { - radius, - alpha = 100, - lineWidth, - } = particle.shapeOptions as PolygonOptions; - if (Array.isArray(alpha)) { - alpha = alpha.map((a) => a * 100) as [number, number]; - } else { - alpha *= 100; - } - const polygons = []; - const sides = sample((particle.shapeOptions as PolygonOptions).sides); - for (let i = 0; i < num; i++) { - const p = new Polygon( - ctx, - x, - y, - particle.colors[anime.random(0, particle.colors.length - 1)], - sample(radius), - sample(alpha) / 100, - sides, - sample(lineWidth) - ); - setEndPos(p, particle); - setEndRotation(p, particle); - polygons.push(p); - } - return polygons; + return preProcess(ctx, x, y, particle, Polygon) as Polygon[]; }; diff --git a/src/index.ts b/src/index.ts index 7014322..126ebe4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,7 @@ import type { FireworkOptions, ParticleOptions, } from "./types"; -import { hasAncestor, sample } from "./utils"; +import { formatAlpha, hasAncestor, sample } from "./utils"; import BaseEntity from "./entity/BaseEntity"; import { createCircle, createStar, createPolygon } from "./factory"; @@ -23,27 +23,26 @@ let pointerX = 0; let pointerY = 0; const setCanvasSize = (): void => { - canvasEl.width = document.documentElement.clientWidth * 2; - canvasEl.height = document.documentElement.clientHeight * 2; - canvasEl.style.width = document.documentElement.clientWidth + "px"; - canvasEl.style.height = document.documentElement.clientHeight + "px"; - - const ctx = canvasEl.getContext("2d"); + const { clientWidth: width, clientHeight: height } = document.documentElement; + canvasEl.width = width * 2; + canvasEl.height = height * 2; + canvasEl.style.width = width + "px"; + canvasEl.style.height = height + "px"; ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.scale(2, 2); }; const updateCoords = (e: MouseEvent | TouchEvent): void => { pointerX = - (e as MouseEvent).clientX || + (e as MouseEvent).clientX ?? ((e as TouchEvent).touches && (e as TouchEvent).touches[0].clientX); pointerY = - (e as MouseEvent).clientY || + (e as MouseEvent).clientY ?? ((e as TouchEvent).touches && (e as TouchEvent).touches[0].clientY); }; const setParticleMovement = (particle: ParticleOptions) => { - const { move } = particle; + const { move, moveOptions } = particle; let dist: Record = {}; if (move.includes("emit")) { const { @@ -51,25 +50,18 @@ const setParticleMovement = (particle: ParticleOptions) => { alphaChange = false, alphaEasing = "linear", alphaDuration = [600, 800], - } = (particle.moveOptions as EmitOptions) ?? {}; - let { alpha = 0 } = (particle.moveOptions as EmitOptions) ?? {}; - if (Array.isArray(alpha)) { - alpha = alpha.map((a) => a * 100) as [number, number]; - } else { - alpha *= 100; - } - let alphaOptions = {}; - if (alphaChange) { - alphaOptions = { - alpha: { - value: sample(alpha) / 100, - easing: alphaEasing, - duration: sample(alphaDuration), - }, - }; - } + } = (moveOptions as EmitOptions) ?? {}; + const { alpha = 0 } = (moveOptions as EmitOptions) ?? {}; + const alphaOptions = alphaChange + ? { + alpha: { + value: sample(formatAlpha(alpha)) / 100, + easing: alphaEasing, + duration: sample(alphaDuration), + }, + } + : {}; dist = { - ...dist, x: (p: BaseEntity) => p.endPos.x, y: (p: BaseEntity) => p.endPos.y, radius: sample(radius), @@ -81,29 +73,20 @@ const setParticleMovement = (particle: ParticleOptions) => { lineWidth = 0, alphaEasing = "linear", alphaDuration = [600, 800], - } = (particle.moveOptions as DiffuseOptions) ?? {}; - let { alpha = 0 } = (particle.moveOptions as DiffuseOptions) ?? {}; - if (Array.isArray(alpha)) { - alpha = alpha.map((a) => a * 100) as [number, number]; - } else { - alpha *= 100; - } + } = (moveOptions as DiffuseOptions) ?? {}; + const { alpha = 0 } = (moveOptions as DiffuseOptions) ?? {}; dist = { - ...dist, radius: sample(diffuseRadius), lineWidth: sample(lineWidth), alpha: { - value: sample(alpha) / 100, + value: sample(formatAlpha(alpha)) / 100, easing: alphaEasing, duration: sample(alphaDuration), }, }; } if (move.includes("rotate")) { - dist = { - ...dist, - rotation: (p: BaseEntity) => p.endRotation, - }; + dist.rotation = (p: BaseEntity) => p.endRotation; } return dist; }; @@ -129,11 +112,13 @@ const initFireworks = (options: FireworkOptions) => { if (currentCallback) { document.removeEventListener(tap, currentCallback, false); } - currentCallback = (e) => { - for (const excludeElement of options.excludeElements) { - if (hasAncestor(e.target, excludeElement)) { - return; - } + currentCallback = (e: MouseEvent | TouchEvent) => { + if ( + options.excludeElements.some((excludeElement) => + hasAncestor(e.target as Element, excludeElement) + ) + ) { + return; } render.play(); updateCoords(e); @@ -149,25 +134,28 @@ const animateParticles = (x: number, y: number): void => { if (!globalOptions) return; const { particles } = globalOptions; const timeLine = anime().timeline(); - for (const particle of particles) { - const { duration, easing } = particle; + particles.forEach((particle) => { let targets = []; - if (particle.shape === "circle") { - targets = createCircle(ctx, x, y, particle); - } else if (particle.shape === "star") { - targets = createStar(ctx, x, y, particle); - } else if (particle.shape === "polygon") { - targets = createPolygon(ctx, x, y, particle); + switch (particle.shape) { + case "circle": + targets = createCircle(ctx, x, y, particle); + break; + case "star": + targets = createStar(ctx, x, y, particle); + break; + case "polygon": + targets = createPolygon(ctx, x, y, particle); + break; } const dist = setParticleMovement(particle); timeLine.add({ targets, - duration: sample(duration), - easing, + duration: sample(particle.duration), + easing: particle.easing ?? "linear", update: renderParticle, ...dist, }); - } + }); timeLine.play(); }; diff --git a/src/utils.ts b/src/utils.ts index 0f48e43..d1f8e21 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -9,10 +9,10 @@ export const sample = (raw: number | [number, number]): number => { export const hasAncestor = (node: Element, name: string): boolean => { name = name.toUpperCase(); - do { - if (node === null || node === undefined) break; + while (node) { if (node.nodeName === name) return true; - } while ((node = node.parentNode) !== null); + node = node.parentNode as Element; + } return false; }; @@ -21,8 +21,7 @@ export const setEndPos = (p: BaseEntity, particle: ParticleOptions) => { let { emitRadius = [50, 180] } = (particle.moveOptions as EmitOptions) ?? {}; const angle = (anime.random(0, 360) * Math.PI) / 180; - emitRadius = sample(emitRadius); - const radius = [-1, 1][anime.random(0, 1)] * emitRadius; + const radius = [-1, 1][anime.random(0, 1)] * sample(emitRadius); p.endPos = { x: p.x + radius * Math.cos(angle), y: p.y + radius * Math.sin(angle), @@ -37,3 +36,10 @@ export const setEndRotation = (p: BaseEntity, particle: ParticleOptions) => { p.endRotation = sample(angle); } }; + +export const formatAlpha = (alpha: number | [number, number]): [number, number] => { + if (Array.isArray(alpha)) { + return alpha.map((a) => a * 100) as [number, number]; + } + return [alpha * 100, alpha * 100]; +} \ No newline at end of file diff --git a/test/index.ts b/test/index.ts new file mode 100644 index 0000000..54cebb4 --- /dev/null +++ b/test/index.ts @@ -0,0 +1,471 @@ +import chai from "chai"; +const should = chai.should(); +import { JSDOM } from "jsdom"; +import sinon from "sinon"; + +const wait = async (time = 0): Promise => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, time); + }); +}; + +describe("firework", () => { + const dom = new JSDOM( + `` + ); + global.document = dom.window.document; + global.navigator = dom.window.navigator; + // @ts-expect-error + global.window = dom.window; + global.HTMLElement = dom.window.HTMLElement; + + const mockCanvas = { + fillRect: () => {}, + clearRect: () => {}, + getImageData: (x, y, w, h) => ({ + data: new Array(w * h * 4), + }), + putImageData: () => {}, + createImageData: () => [], + setTransform: () => {}, + drawImage: () => {}, + save: () => {}, + fillText: () => {}, + restore: () => {}, + beginPath: () => {}, + moveTo: () => {}, + lineTo: () => {}, + closePath: () => {}, + stroke: () => {}, + translate: () => {}, + scale: () => {}, + rotate: () => {}, + arc: () => {}, + fill: () => {}, + measureText: () => ({ width: 0 }), + transform: () => {}, + rect: () => {}, + clip: () => {}, + lineWidth: 0, + fillStyle: "", + strokeStyle: "", + globalAlpha: 1, + }; + + // @ts-expect-error + window.HTMLCanvasElement.prototype.getContext = () => mockCanvas; + + window.HTMLCanvasElement.prototype.toDataURL = () => ""; + + it("base call raf", async () => { + const spy = sinon.spy(); + global.requestAnimationFrame = spy; + const { default: firework } = await import("../src/index"); + firework({ + excludeElements: [], + particles: [ + { + shape: "circle", + move: ["emit"], + colors: ["rgba(255,182,185,.9)"], + number: 30, + duration: [1200, 1800], + shapeOptions: { + radius: [16, 32], + }, + }, + ], + }); + document.dispatchEvent(new window.MouseEvent("click", { bubbles: true })); + await wait(); + spy.called.should.be.true; + }); + + it("excludeElements", async () => { + const spy = sinon.spy(); + global.requestAnimationFrame = spy; + const { default: firework } = await import("../src/index"); + firework({ + excludeElements: ["button"], + particles: [ + { + shape: "circle", + move: ["emit"], + colors: ["rgba(255,182,185,.9)"], + number: 30, + duration: [1200, 1800], + shapeOptions: { + radius: [16, 32], + }, + }, + ], + }); + + document + .getElementById("test")! + .dispatchEvent(new window.MouseEvent("click", { bubbles: true })); + await wait(); + spy.called.should.be.false; + }); + + it("stroke shape - base entity", async () => { + const translateSpy = sinon.spy(); + const rotateSpy = sinon.spy(); + const strokeSpy = sinon.spy(); + const arcSpy = () => { + mockCanvas.globalAlpha.should.eql(0.5); + }; + global.requestAnimationFrame = () => 0; + mockCanvas.translate = translateSpy; + mockCanvas.rotate = rotateSpy; + mockCanvas.stroke = strokeSpy; + mockCanvas.arc = arcSpy; + const { default: firework } = await import("../src/index"); + firework({ + excludeElements: [], + particles: [ + { + shape: "circle", + move: [], + colors: ["rgba(255,182,185)"], + number: 1, + duration: 1000, + shapeOptions: { + radius: 10, + alpha: 0.5, + lineWidth: 2, + }, + }, + ], + }); + document.dispatchEvent( + new window.MouseEvent("click", { bubbles: true, clientX: 0, clientY: 0 }) + ); + await wait(); + translateSpy.args[0].should.eql([0, 0]); + rotateSpy.args[0].should.eql([0]); + strokeSpy.called.should.be.true; + mockCanvas.lineWidth.should.eql(2); + mockCanvas.strokeStyle.should.eql("rgba(255,182,185)"); + + mockCanvas.lineWidth = 0; + mockCanvas.strokeStyle = ""; + mockCanvas.arc = () => {}; + }); + + it("fill shape - base entity", async () => { + const translateSpy = sinon.spy(); + const rotateSpy = sinon.spy(); + const fillSpy = sinon.spy(); + global.requestAnimationFrame = () => 0; + mockCanvas.translate = translateSpy; + mockCanvas.rotate = rotateSpy; + mockCanvas.fill = fillSpy; + const { default: firework } = await import("../src/index"); + firework({ + excludeElements: [], + particles: [ + { + shape: "circle", + move: [], + colors: ["rgba(255,182,185)"], + number: 1, + duration: 1000, + shapeOptions: { + radius: 10, + }, + }, + ], + }); + document.dispatchEvent( + new window.MouseEvent("click", { bubbles: true, clientX: 0, clientY: 0 }) + ); + await wait(); + translateSpy.args[0].should.eql([0, 0]); + rotateSpy.args[0].should.eql([0]); + fillSpy.called.should.be.true; + + mockCanvas.fillStyle.should.eql("rgba(255,182,185)"); + mockCanvas.fillStyle = ""; + }); + + it("shape - circle", async () => { + const beginPathSpy = sinon.spy(); + const arcSpy = sinon.spy(); + const closePathSpy = sinon.spy(); + global.requestAnimationFrame = () => 0; + mockCanvas.beginPath = beginPathSpy; + mockCanvas.arc = arcSpy; + mockCanvas.closePath = closePathSpy; + const { default: firework } = await import("../src/index"); + firework({ + excludeElements: [], + particles: [ + { + shape: "circle", + move: [], + colors: ["rgba(255,182,185)"], + number: 1, + duration: 1000, + shapeOptions: { + radius: 10, + }, + }, + ], + }); + + document.dispatchEvent(new window.MouseEvent("click", { bubbles: true })); + await wait(); + beginPathSpy.called.should.be.true; + arcSpy.args[0].should.eql([0, 0, 10, 0, 2 * Math.PI]); + closePathSpy.called.should.be.true; + }); + + it("shape - star", async () => { + const beginPathSpy = sinon.spy(); + const moveToSpy = sinon.spy(); + const lineToSpy = sinon.spy(); + const closePathSpy = sinon.spy(); + global.requestAnimationFrame = () => 0; + mockCanvas.beginPath = beginPathSpy; + mockCanvas.moveTo = moveToSpy; + mockCanvas.lineTo = lineToSpy; + mockCanvas.closePath = closePathSpy; + const { default: firework } = await import("../src/index"); + firework({ + excludeElements: [], + particles: [ + { + shape: "star", + move: [], + colors: ["rgba(255,182,185)"], + number: 1, + duration: 1000, + shapeOptions: { + radius: 10, + spikes: 4, + }, + }, + ], + }); + + document.dispatchEvent(new window.MouseEvent("click", { bubbles: true })); + await wait(); + beginPathSpy.called.should.be.true; + moveToSpy.args[0].should.eql([0, -10]); + lineToSpy.callCount.should.eql(8); + const baseSize = (5 * Math.sqrt(2)) / 2; + (Math.abs(lineToSpy.args[0][0] - 0) < 1e-10).should.be.true; + (Math.abs(lineToSpy.args[0][1] + 10) < 1e-10).should.be.true; + (Math.abs(lineToSpy.args[1][0] - baseSize) < 1e-10).should.be.true; + (Math.abs(lineToSpy.args[1][1] + baseSize) < 1e-10).should.be.true; + (Math.abs(lineToSpy.args[2][0] - 10) < 1e-10).should.be.true; + (Math.abs(lineToSpy.args[2][1] + 0) < 1e-10).should.be.true; + (Math.abs(lineToSpy.args[3][0] - baseSize) < 1e-10).should.be.true; + (Math.abs(lineToSpy.args[3][1] - baseSize) < 1e-10).should.be.true; + (Math.abs(lineToSpy.args[4][0] + 0) < 1e-10).should.be.true; + (Math.abs(lineToSpy.args[4][1] - 10) < 1e-10).should.be.true; + (Math.abs(lineToSpy.args[5][0] + baseSize) < 1e-10).should.be.true; + (Math.abs(lineToSpy.args[5][1] - baseSize) < 1e-10).should.be.true; + (Math.abs(lineToSpy.args[6][0] + 10) < 1e-10).should.be.true; + (Math.abs(lineToSpy.args[6][1] - 0) < 1e-10).should.be.true; + (Math.abs(lineToSpy.args[7][0] + baseSize) < 1e-10).should.be.true; + (Math.abs(lineToSpy.args[7][1] + baseSize) < 1e-10).should.be.true; + closePathSpy.called.should.be.true; + }); + + it("shape - polygon", async () => { + const beginPathSpy = sinon.spy(); + const moveToSpy = sinon.spy(); + const lineToSpy = sinon.spy(); + const closePathSpy = sinon.spy(); + global.requestAnimationFrame = () => 0; + mockCanvas.beginPath = beginPathSpy; + mockCanvas.moveTo = moveToSpy; + mockCanvas.lineTo = lineToSpy; + mockCanvas.closePath = closePathSpy; + const { default: firework } = await import("../src/index"); + firework({ + excludeElements: [], + particles: [ + { + shape: "polygon", + move: [], + colors: ["rgba(255,182,185)"], + number: 1, + duration: 1000, + shapeOptions: { + radius: 10, + sides: 4, + }, + }, + ], + }); + + document.dispatchEvent(new window.MouseEvent("click", { bubbles: true })); + await wait(); + beginPathSpy.called.should.be.true; + moveToSpy.args[0].should.eql([10, 0]); + lineToSpy.callCount.should.eql(4); + (Math.abs(lineToSpy.args[0][0] - 0) < 1e-10).should.be.true; + (Math.abs(lineToSpy.args[0][1] - 10) < 1e-10).should.be.true; + (Math.abs(lineToSpy.args[1][0] + 10) < 1e-10).should.be.true; + (Math.abs(lineToSpy.args[1][1] + 0) < 1e-10).should.be.true; + (Math.abs(lineToSpy.args[2][0] + 0) < 1e-10).should.be.true; + (Math.abs(lineToSpy.args[2][1] + 10) < 1e-10).should.be.true; + (Math.abs(lineToSpy.args[3][0] - 10) < 1e-10).should.be.true; + (Math.abs(lineToSpy.args[3][1] + 0) < 1e-10).should.be.true; + closePathSpy.called.should.be.true; + }); + + it("emit", async () => { + let i = 0; + const translateSpy = sinon.spy(); + const arcSpy = sinon.spy(); + mockCanvas.translate = translateSpy; + mockCanvas.arc = (...args) => { + arcSpy(...args); + const radius = + translateSpy.args[i][0] ** 2 + translateSpy.args[i][1] ** 2; + (Math.abs(radius - (i * 2) ** 2) < 1e-10).should.be.true; + arcSpy.args[i][2].should.eql(10 - 2 * i); + if (i < 3) { + mockCanvas.globalAlpha.should.eql(1 - i / 3); + } + i++; + }; + + Date.now = () => i * 200; + global.requestAnimationFrame = (cb) => { + if (i > 5) { + global.requestAnimationFrame = () => 0; + } + setTimeout(cb, 0); + return 0; + }; + const { default: firework } = await import("../src/index"); + firework({ + excludeElements: ["button"], + particles: [ + { + shape: "circle", + move: ["emit"], + colors: ["rgba(255,182,185)"], + number: 1, + duration: 1000, + shapeOptions: { + radius: 10, + }, + moveOptions: { + emitRadius: 10, + radius: 0, + alphaChange: true, + alpha: 0, + alphaEasing: "linear", + alphaDuration: 600, + }, + }, + ], + }); + document.dispatchEvent(new window.MouseEvent("click", { bubbles: true })); + await wait(1200); + }); + + it("diffuse", async () => { + let i = 0; + const translateSpy = sinon.spy(); + const arcSpy = sinon.spy(); + const strokeSpy = sinon.spy(); + mockCanvas.translate = translateSpy; + mockCanvas.arc = (...args) => { + arcSpy(...args); + translateSpy.args[i].should.eql([0, 0]); + arcSpy.args[i][2].should.eql(10 + 2 * i); + if (i < 3) { + mockCanvas.globalAlpha.should.eql(1 - i / 3); + } + }; + mockCanvas.stroke = (...args) => { + strokeSpy(...args); + mockCanvas.lineWidth.should.eql(5 - i); + i++; + }; + + Date.now = () => i * 200; + global.requestAnimationFrame = (cb) => { + if (i > 4) { + global.requestAnimationFrame = () => 0; + } + setTimeout(cb, 0); + return 0; + }; + const { default: firework } = await import("../src/index"); + firework({ + excludeElements: ["button"], + particles: [ + { + shape: "circle", + move: ["diffuse"], + colors: ["rgba(255,182,185)"], + number: 1, + duration: 1000, + shapeOptions: { + radius: 10, + lineWidth: 5, + }, + moveOptions: { + diffuseRadius: 20, + lineWidth: 0, + alpha: 0, + alphaEasing: "linear", + alphaDuration: 600, + }, + }, + ], + }); + document.dispatchEvent(new window.MouseEvent("click", { bubbles: true })); + await wait(1200); + }); + + it("rotate", async () => { + let i = 0; + const rotateSpy = sinon.spy(); + mockCanvas.rotate = (...args) => { + rotateSpy(...args); + rotateSpy.args[i][0].should.eql(Math.PI / 5 * i); + i++; + }; + + Date.now = () => i * 200; + global.requestAnimationFrame = (cb) => { + if (i > 5) { + global.requestAnimationFrame = () => 0; + } + setTimeout(cb, 0); + return 0; + }; + const { default: firework } = await import("../src/index"); + firework({ + excludeElements: ["button"], + particles: [ + { + shape: "circle", + move: ["rotate"], + colors: ["rgba(255,182,185)"], + number: 1, + duration: 1000, + shapeOptions: { + radius: 10, + }, + moveOptions: { + angle: 180, + }, + }, + ], + }); + document.dispatchEvent(new window.MouseEvent("click", { bubbles: true })); + await wait(1200); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index fdaca10..115ff6e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "esModuleInterop": true, "types": [ "node", + "mocha" ], "lib": ["dom", "ESNext"] }, From 2f50b33608620d493d54993779004ea92eadf639 Mon Sep 17 00:00:00 2001 From: D-Sketon <2055272094@qq.com> Date: Mon, 15 Jul 2024 21:57:48 +0800 Subject: [PATCH 2/3] chore: coveralls --- .github/workflows/tester.yml | 68 ++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 .github/workflows/tester.yml diff --git a/.github/workflows/tester.yml b/.github/workflows/tester.yml new file mode 100644 index 0000000..583ce73 --- /dev/null +++ b/.github/workflows/tester.yml @@ -0,0 +1,68 @@ +name: Tester + +on: + push: + branches: + - "main" + paths: + - "src/**" + - "test/**" + - "package.json" + - "tsconfig.json" + - ".github/workflows/tester.yml" + pull_request: + paths: + - "src/**" + - "test/**" + - "package.json" + - "tsconfig.json" + - ".github/workflows/tester.yml" + +permissions: + contents: read + +jobs: + tester: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + node-version: ["18.x"] + fail-fast: false + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - name: Install Dependencies + run: npm install + - name: Test + run: npm test + env: + CI: true + coverage: + permissions: + checks: write # for coverallsapp/github-action to create new checks + contents: read # for actions/checkout to fetch code + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + node-version: ["18.x"] + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - name: Install Dependencies + run: npm install + - name: Coverage + run: npm run test-cov + env: + CI: true + - name: Coveralls + uses: coverallsapp/github-action@master + with: + github-token: ${{ github.token }} \ No newline at end of file From f536164d4f3871264919c8145db3687e0cf517c5 Mon Sep 17 00:00:00 2001 From: D-Sketon <2055272094@qq.com> Date: Mon, 15 Jul 2024 22:03:22 +0800 Subject: [PATCH 3/3] chore: README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6c19adf..f8360b7 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ NPM Bundle Size NPM Downloads jsDelivr hits (npm) + Coverage Status [Demo](https://d-sketon.github.io/mouse-firework) @@ -27,7 +28,7 @@ npm i mouse-firework --save or just use it in your browser ```html - + ``` ### Basic Usage