diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c3666436..50f7fd80 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -70,8 +70,8 @@ jobs: - uses: browser-actions/setup-chrome@v1 with: chrome-version: 133.0.6943.98 - # - name: Install Playwright browsers - # run: npx playwright install --with-deps + - name: Install Playwright browsers + run: npx playwright install chromium --with-deps - name: Tests run: npm run test:ci -- --coverage=${{ matrix.runner == 'ubuntu-latest' }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f1fa7e1..5dc1e3da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added +- Fixed ex.Font measureText always using 10px sans-serif on the first call on some browsers - Added a new `ex.Sound({...})` option back constructor to set all the same props available on sound - Added a new `ex.SoundManager` type for managing groups of audio/sound effects/music volume in an easier way diff --git a/package.json b/package.json index 49605f22..d0cbe3a3 100644 --- a/package.json +++ b/package.json @@ -65,10 +65,11 @@ "sandbox": "cd sandbox && vite", "test": "vitest", "test:watch": "npm run test -- --watch", - "test:ci": "npm run test:chromium -- --silent", - "test:chromium": "vitest --browser chromium", - "test:firefox": "vitest --browser firefox", - "test:webkit": "vitest --browser webkit", + "test:unit": "vitest --project unit", + "test:unit:headless": "npm run test:unit -- --browser.headless", + "test:visual": "vitest --project visual", + "test:visual:headless": "npm run test:visual -- --browser.headless", + "test:ci": "npm run test -- --silent --hideSkippedTests", "apidocs": "node scripts/apidocs.js", "release": "cross-env release=true npm run all", "prepare": "husky" diff --git a/src/engine/Graphics/Font.ts b/src/engine/Graphics/Font.ts index d8555c89..b4168e56 100644 --- a/src/engine/Graphics/Font.ts +++ b/src/engine/Graphics/Font.ts @@ -53,6 +53,9 @@ export class Font extends Graphic implements FontRenderer { this.shadow.offset = options.shadow.offset ?? this.shadow.offset; this.shadow.color = options.shadow.color ?? this.shadow.color; } + + // must be created after this is finished setting values + this._textMeasurement = new FontTextInstance(this, '', Color.Black); } public clone() { @@ -142,7 +145,7 @@ export class Font extends Graphic implements FontRenderer { } } - private _textMeasurement = new FontTextInstance(this, '', Color.Black); + private _textMeasurement: FontTextInstance; public measureTextWithoutCache(text: string, maxWidth?: number) { return this._textMeasurement.measureText(text, maxWidth); diff --git a/src/spec/__matchers__/expect.ts b/src/spec/__matchers__/expect.ts new file mode 100644 index 00000000..e7bbf05a --- /dev/null +++ b/src/spec/__matchers__/expect.ts @@ -0,0 +1,81 @@ +// port of excalibur-jasmine for jest. might be a good idea to publish this as its own package. +import type { ExcaliburVisual } from './image-helpers'; +import { convertSourceVisualToImageData, compareImageData } from './image-helpers'; + +import type * as ex from '@excalibur'; +import { expect } from 'vitest'; + +expect.extend({ + // users will get a type error as we only type this in spec/visual files + // but incase they run the test anyway, we'll throw a helpful error + toEqualImage: () => { + throw new Error('toEqualImage assertion can only be used in visual tests, please add "@visual" to the test name.'); + }, + + toBeVector: (actual: ex.Vector, expected: ex.Vector, delta: number = 0.01) => { + const distance = actual.distance(expected); + if (distance <= delta) { + return { + pass: true, + message: () => `Vector within delta ${distance} <= ${delta}` + }; + } else { + return { + pass: false, + message: () => + `Expected ex.Vector${actual.toString()} to be within ${delta} of ex.Vector${expected.toString()}, but was ${distance} distance apart` + }; + } + }, + toHaveValues: (actual: ex.Actor, expected: ex.ActorArgs) => { + let message = 'Expected actor to have properties:\r\n\r\n'; + let passed = true; + for (const key in expected) { + if (actual[key] !== expected[key]) { + passed = false; + message += `Expected actor.${key} to be ${expected[key]}, but got ${actual[key]}\r\n`; + } + } + + return { + pass: passed, + message: () => (passed ? 'Actor properties match' : message) + }; + }, + toHaveLoadedImages: async (actual: HTMLCanvasElement, images: ExcaliburVisual[], tolerance: number = 0.995) => { + const results: Promise[] = []; + for (const image of images) { + results.push(convertSourceVisualToImageData(image)); + } + + const data = await Promise.all(results); + + let pass = true; + let msg: string | undefined; + + for (const imgData of data) { + const result = await compareImageData(actual, imgData, tolerance); + if (!result.pass) { + pass = false; + msg = result.message(); + break; + } + } + + return { + pass, + message: () => msg + }; + } +}); + +interface CustomMatchers { + toBeVector(expected: ex.Vector, delta?: number): R; + toHaveValues(expected: ex.ActorArgs): R; + toHaveLoadedImages(images: ExcaliburVisual[], tolerance?: number): Promise; +} + +declare module 'vitest' { + interface Assertion extends CustomMatchers {} + interface AsymmetricMatchersContaining extends CustomMatchers {} +} diff --git a/src/spec/__matchers__/expect.visual.ts b/src/spec/__matchers__/expect.visual.ts new file mode 100644 index 00000000..b39233e6 --- /dev/null +++ b/src/spec/__matchers__/expect.visual.ts @@ -0,0 +1,24 @@ +// this adds assertions only to be used for visual tests +import type { Visual } from './image-helpers'; +import { compareImageData } from './image-helpers'; +import { expect } from 'vitest'; + +expect.extend({ + toEqualImage: async (actual: Visual, expected: Visual, tolerance: number = 0.995) => { + return await compareImageData(actual, expected, tolerance); + } +}); + +interface CustomMatchers { + /** + * Compare two images for pixel equality + * @param expected - The expected image to compare against + * @param tolerance - The difference tolerance level. Default is 0.995 + */ + toEqualImage(expected: Visual, tolerance?: number): Promise; +} + +declare module 'vitest' { + interface Assertion extends CustomMatchers {} + interface AsymmetricMatchersContaining extends CustomMatchers {} +} diff --git a/src/spec/vitest/__setup__/matchers/expect.ts b/src/spec/__matchers__/image-helpers.ts similarity index 60% rename from src/spec/vitest/__setup__/matchers/expect.ts rename to src/spec/__matchers__/image-helpers.ts index 117a95a4..bba9bc19 100644 --- a/src/spec/vitest/__setup__/matchers/expect.ts +++ b/src/spec/__matchers__/image-helpers.ts @@ -1,20 +1,23 @@ /* eslint-disable no-console */ -// port of excalibur-jasmine for jest. might be a good idea to publish this as its own package. -import { convertSourceVisualToImageData, ImageVisual } from './convert'; import pixelmatch from 'pixelmatch'; -import type * as ex from '@excalibur'; -import { expect } from 'vitest'; - -// seems to not exist in pixelmatch? -type PixelmatchOptions = any; - -export declare type ExcaliburVisual = string | HTMLImageElement | HTMLCanvasElement | CanvasRenderingContext2D; - -export type Visual = string | HTMLImageElement | HTMLCanvasElement | CanvasRenderingContext2D | ImageData; -export type DiffOptions = PixelmatchOptions; +type ImageVisual = HTMLCanvasElement | CanvasRenderingContext2D | HTMLImageElement | string; +export const convertSourceVisualToImageData = async (visual: ImageVisual): Promise => { + if (visual instanceof HTMLCanvasElement) { + return convertCanvas(visual); + } + if (visual instanceof CanvasRenderingContext2D) { + return convertContext(visual); + } + if (visual instanceof HTMLImageElement) { + return await convertImage(visual); + } + if (typeof visual === 'string') { + return await convertFilePath(visual); + } +}; -const flushSourceToImageData = async (source: Visual) => { +export const flushSourceToImageData = async (source: Visual) => { const canvas = document.createElement('canvas'); const context = canvas.getContext('2d')!; @@ -77,7 +80,7 @@ const flushSourceToImageData = async (source: Visual) => { return [context.getImageData(0, 0, context.canvas.width, context.canvas.height), context] as const; }; -const compareImageData = async (actual: Visual, expected: Visual, tolerance: number) => { +export const compareImageData = async (actual: Visual, expected: Visual, tolerance: number) => { const [actualData, actualCtx] = await flushSourceToImageData(actual); const [expectedData, expectedCtx] = await flushSourceToImageData(expected); @@ -124,81 +127,63 @@ const compareImageData = async (actual: Visual, expected: Visual, tolerance: num } }; -expect.extend({ - toEqualImage: async (actual: Visual, expected: Visual, tolerance: number = 0.995) => { - return await compareImageData(actual, expected, tolerance); - }, - - toBeVector: (actual: ex.Vector, expected: ex.Vector, delta: number = 0.01) => { - const distance = actual.distance(expected); - if (distance <= delta) { - return { - pass: true, - message: () => `Vector within delta ${distance} <= ${delta}` - }; - } else { - return { - pass: false, - message: () => - `Expected ex.Vector${actual.toString()} to be within ${delta} of ex.Vector${expected.toString()}, but was ${distance} distance apart` - }; - } - }, - toHaveValues: (actual: ex.Actor, expected: ex.ActorArgs) => { - let message = 'Expected actor to have properties:\r\n\r\n'; - let passed = true; - for (const key in expected) { - if (actual[key] !== expected[key]) { - passed = false; - message += `Expected actor.${key} to be ${expected[key]}, but got ${actual[key]}\r\n`; - } - } +const convertContext = (visual: CanvasRenderingContext2D): ImageData => { + return visual.getImageData(0, 0, visual.canvas.width, visual.canvas.height); +}; - return { - pass: passed, - message: () => (passed ? 'Actor properties match' : message) - }; - }, - toHaveLoadedImages: async (actual: HTMLCanvasElement, images: ExcaliburVisual[], tolerance: number = 0.995) => { - const results: Promise[] = []; - for (const image of images) { - results.push(convertSourceVisualToImageData(image)); +const convertCanvas = (canvas: HTMLCanvasElement): ImageData => { + const ctx = canvas.getContext('2d'); + const data = ctx.getImageData(0, 0, canvas.width, canvas.height); + return data; +}; + +const convertFilePath = async (imagePath: string, baseImagePath: string = ''): Promise => { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + const img = new Image(); + img.decoding = 'sync'; + if (imagePath) { + img.src = baseImagePath + imagePath + '?_=' + Math.random(); + try { + await img.decode(); + } catch { + console.warn(`Image could not be decoded, check image src: ${img.src}`); } + } - const data = await Promise.all(results); + canvas.width = img.width; + canvas.height = img.height; - let pass = true; - let msg: string | undefined; + ctx.drawImage(img, 0, 0); + return ctx.getImageData(0, 0, img.width, img.height); +}; - for (const imgData of data) { - const result = await compareImageData(actual, imgData, tolerance); - if (!result.pass) { - pass = false; - msg = result.message(); - break; - } - } +const convertImage = async (image: HTMLImageElement): Promise => { + const canvas = document.createElement('canvas'); + image.decoding = 'sync'; - return { - pass, - message: () => msg - }; + if (image.src) { + try { + await image.decode(); + } catch { + console.warn(`Image could not be decoded, check image src: ${image.src}`); + } } -}); - -interface CustomMatchers { - /** - * Compare two images for pixel equality - * @param expected - The expected image to compare against - * @param tolerance - The difference tolerance level. Default is 0.995 - */ - toEqualImage(expected: Visual, tolerance?: number): Promise; - toBeVector(expected: ex.Vector, delta?: number): R; - toHaveValues(expected: ex.ActorArgs): R; - toHaveLoadedImages(images: ExcaliburVisual[], tolerance?: number): Promise; -} - -declare module 'vitest' { - interface Assertion extends CustomMatchers {} - interface AsymmetricMatchersContaining extends CustomMatchers {} -} + + canvas.width = image.width; + canvas.height = image.height; + const ctx = canvas.getContext('2d'); + ctx.drawImage(image, 0, 0); + + return ctx.getImageData(0, 0, image.width, image.height); +}; + +export type { ImageVisual }; + +// seems to not exist in pixelmatch? +type PixelmatchOptions = any; +export declare type ExcaliburVisual = string | HTMLImageElement | HTMLCanvasElement | CanvasRenderingContext2D; + +export type Visual = string | HTMLImageElement | HTMLCanvasElement | CanvasRenderingContext2D | ImageData; +export type DiffOptions = PixelmatchOptions; diff --git a/src/spec/vitest/util/Mocks.ts b/src/spec/__util__/Mocks.ts similarity index 100% rename from src/spec/vitest/util/Mocks.ts rename to src/spec/__util__/Mocks.ts diff --git a/src/spec/vitest/util/TestUtils.ts b/src/spec/__util__/TestUtils.ts similarity index 100% rename from src/spec/vitest/util/TestUtils.ts rename to src/spec/__util__/TestUtils.ts diff --git a/src/spec/vitest/__reporters__/engine-instance.setup.ts b/src/spec/__util__/reporters/engine-instance.setup.ts similarity index 100% rename from src/spec/vitest/__reporters__/engine-instance.setup.ts rename to src/spec/__util__/reporters/engine-instance.setup.ts diff --git a/src/spec/vitest/__reporters__/engine-instance.ts b/src/spec/__util__/reporters/engine-instance.ts similarity index 100% rename from src/spec/vitest/__reporters__/engine-instance.ts rename to src/spec/__util__/reporters/engine-instance.ts diff --git a/src/spec/vitest/__reporters__/memory.setup.ts b/src/spec/__util__/reporters/memory.setup.ts similarity index 100% rename from src/spec/vitest/__reporters__/memory.setup.ts rename to src/spec/__util__/reporters/memory.setup.ts diff --git a/src/spec/vitest/__reporters__/memory.ts b/src/spec/__util__/reporters/memory.ts similarity index 100% rename from src/spec/vitest/__reporters__/memory.ts rename to src/spec/__util__/reporters/memory.ts diff --git a/src/spec/vitest/__setup__/setup.ts b/src/spec/__util__/setup.ts similarity index 64% rename from src/spec/vitest/__setup__/setup.ts rename to src/spec/__util__/setup.ts index d38bdeb5..2e911ab5 100644 --- a/src/spec/vitest/__setup__/setup.ts +++ b/src/spec/__util__/setup.ts @@ -1,8 +1,7 @@ import * as ex from '@excalibur'; import { beforeAll } from 'vitest'; -import './matchers/expect'; -import { setupEngineInstanceReporter } from '../__reporters__/engine-instance.setup'; -import { setupMemoryReporter } from '../__reporters__/memory.setup'; +import { setupEngineInstanceReporter } from './reporters/engine-instance.setup'; +import { setupMemoryReporter } from './reporters/memory.setup'; setupEngineInstanceReporter(); setupMemoryReporter(); diff --git a/src/spec/tsconfig.json b/src/spec/tsconfig.json new file mode 100644 index 00000000..7cb2921c --- /dev/null +++ b/src/spec/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "types": ["@vitest/browser/providers/playwright", "vitest/globals"], + "moduleResolution": "node", + "sourceMap": true, + "experimentalDecorators": true, + "target": "es2015", + "module": "es2015", + "noImplicitAny": false, + "noUnusedLocals": false, + "downlevelIteration": true, + "allowSyntheticDefaultImports": true, + "lib": [ + "dom", + "dom.iterable", + "es5", + "es2015.collection", + "es2015.iterable", + "es2015.promise", + "es2015.proxy", + "es2018.promise", + "es2016", + "es2017.object", + "es2018" + ], + "baseUrl": ".", + "rootDir": "../../", + "paths": { + "@excalibur": ["../engine/"] + } + }, + "include": ["./__util__/**/*.ts", "./__matchers__/**/*.ts", "./vitest.*.ts"], + "files": ["../engine/files.d.ts", "../engine/globals.d.ts"] +} diff --git a/src/spec/vitest.config.unit.ts b/src/spec/vitest.config.unit.ts new file mode 100644 index 00000000..689f0617 --- /dev/null +++ b/src/spec/vitest.config.unit.ts @@ -0,0 +1,43 @@ +/// + +import * as os from 'os'; +import type { ViteUserConfig } from 'vitest/config'; +import { defineConfig, mergeConfig } from 'vitest/config'; +import baseConfig from '../../vitest.config'; + +export default defineConfig( + mergeConfig(baseConfig, { + test: { + name: 'unit', + globals: true, + setupFiles: ['./__util__/setup.ts', './__matchers__/expect.ts'], + include: ['./vitest/**/*Spec.ts'], + testNamePattern: /^(?!.*@visual).+$/, + sequence: { + groupOrder: 0 + }, + browser: { + enabled: true, + provider: 'playwright', + // this will give each test their own environment. disabling this + // actually ended up breaking WebGL contexts in some cases + isolate: true, + headless: process.env.CI === 'true' ? true : undefined, + + instances: [ + { + browser: 'chromium', + provide: { + browser: 'chromium', + platform: os.platform() + }, + launch: { + channel: 'chromium', + args: ['--mute-audio', '--enable-precise-memory-info', '--js-flags="--max_old_space_size=8192" --expose-gc'].filter(Boolean) + } + } + ] + } + } + } satisfies ViteUserConfig) +); diff --git a/src/spec/vitest.config.visual.ts b/src/spec/vitest.config.visual.ts new file mode 100644 index 00000000..21f14ac2 --- /dev/null +++ b/src/spec/vitest.config.visual.ts @@ -0,0 +1,68 @@ +/// + +import * as os from 'os'; +import type { ViteUserConfig } from 'vitest/config'; +import { defineConfig, mergeConfig } from 'vitest/config'; +import baseConfig from '../../vitest.config'; + +export default defineConfig( + mergeConfig(baseConfig, { + test: { + name: 'visual', + globals: true, + setupFiles: ['./__util__/setup.ts', './__matchers__/expect.ts', './__matchers__/expect.visual.ts'], + include: ['./vitest/**/*Spec.ts'], + testNamePattern: /@visual/, + sequence: { + groupOrder: 1 + }, + browser: { + enabled: true, + provider: 'playwright', + // this will give each test their own environment. disabling this + // ended up breaking WebGL contexts in some cases + isolate: true, + + headless: process.env.CI === 'true' ? true : undefined, + + instances: [ + { + browser: 'chromium', + provide: { + browser: 'chromium', + platform: os.platform() + }, + launch: { + channel: 'chromium', + // we pass in a whole bunch of args to try and make rendering as consistent as possible across + // operating systems. + ignoreDefaultArgs: ['--disable-render-backgrounding', '--disable-remote-fonts', '--font-render-hinting'], + args: [ + '--no-default-browser-check', + '--no-first-run', + '--disable-default-apps', + '--disable-popup-blocking', + '--disable-translate', + '--disable-background-timer-throttling', + '--disable-gpu', + '--disable-dev-shm-usage', + + // on macOS, disable-background-timer-throttling is not enough + // and we need disable-renderer-backgrounding too + // see https://github.com/karma-runner/karma-chrome-launcher/issues/123 + '--disable-renderer-backgrounding', + '--disable-device-discovery-notifications', + + '--autoplay-policy=no-user-gesture-required', + '--mute-audio', + '--no-sandbox', + '--enable-precise-memory-info', + '--js-flags="--max_old_space_size=8192" --expose-gc' + ].filter(Boolean) + } + } + ] + } + } + } satisfies ViteUserConfig) +); diff --git a/src/spec/vitest/ActionSpec.ts b/src/spec/vitest/ActionSpec.ts index b5504859..919602cf 100644 --- a/src/spec/vitest/ActionSpec.ts +++ b/src/spec/vitest/ActionSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('Action', () => { let actor: ex.Actor; diff --git a/src/spec/vitest/ActorSpec.ts b/src/spec/vitest/ActorSpec.ts index 68cac7ad..2f0304e7 100644 --- a/src/spec/vitest/ActorSpec.ts +++ b/src/spec/vitest/ActorSpec.ts @@ -1,7 +1,7 @@ import * as ex from '@excalibur'; import { PhysicsWorld } from '../../engine/Collision/PhysicsWorld'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('A game actor', () => { let actor: ex.Actor; @@ -756,7 +756,7 @@ describe('A game actor', () => { expect(invisibleActor.graphics.onPostDraw).toHaveBeenCalled(); }); - it('can be drawn with a z-index', async () => { + it('@visual can be drawn with a z-index', async () => { engine.stop(); engine.dispose(); engine = null; @@ -798,7 +798,7 @@ describe('A game actor', () => { await expect(engine.canvas).toEqualImage('/src/spec/assets/images/ActorSpec/zindex-green-top.png'); }); - it('can have a graphic drawn at an opacity', async () => { + it('@visual can have a graphic drawn at an opacity', async () => { engine.stop(); engine.dispose(); engine = null; diff --git a/src/spec/vitest/AnimationSpec.ts b/src/spec/vitest/AnimationSpec.ts index ebfef1e9..d2e2da6f 100644 --- a/src/spec/vitest/AnimationSpec.ts +++ b/src/spec/vitest/AnimationSpec.ts @@ -461,7 +461,7 @@ describe('A Graphics Animation', () => { expect(onFrame).toHaveBeenCalledTimes(2); }); - it('draws the right frame to the screen', async () => { + it('@visual draws the right frame to the screen', async () => { const rect = new ex.Rectangle({ width: 100, height: 100, diff --git a/src/spec/vitest/ArcadeSolverSpec.ts b/src/spec/vitest/ArcadeSolverSpec.ts index 2f20b1d6..a0426bb1 100644 --- a/src/spec/vitest/ArcadeSolverSpec.ts +++ b/src/spec/vitest/ArcadeSolverSpec.ts @@ -1,6 +1,6 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; import { getDefaultPhysicsConfig } from '../../engine/Collision/PhysicsConfig'; describe('An ArcadeSolver', () => { diff --git a/src/spec/vitest/CameraSpec.ts b/src/spec/vitest/CameraSpec.ts index 4fa29601..d46161cf 100644 --- a/src/spec/vitest/CameraSpec.ts +++ b/src/spec/vitest/CameraSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('A camera', () => { let Camera: ex.Camera; diff --git a/src/spec/vitest/CanvasSpec.ts b/src/spec/vitest/CanvasSpec.ts index d62587eb..102f5362 100644 --- a/src/spec/vitest/CanvasSpec.ts +++ b/src/spec/vitest/CanvasSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('A Canvas Graphic', () => { it('exists', () => { @@ -30,7 +30,7 @@ describe('A Canvas Graphic', () => { expect(sut.height).toBe(clone.height); }); - it('can be drawn using the 2d canvas api', async () => { + it('@visual can be drawn using the 2d canvas api', async () => { const sut = new ex.Canvas({ width: 100, height: 100, @@ -102,7 +102,7 @@ describe('A Canvas Graphic', () => { expect(sut.execute).toHaveBeenCalledTimes(4); }); - it('can be centered with the specified dimensions in an actor', async () => { + it('@visual can be centered with the specified dimensions in an actor', async () => { const engine = TestUtils.engine({ width: 100, height: 100 }); const clock = engine.clock as ex.TestClock; await TestUtils.runToReady(engine); diff --git a/src/spec/vitest/CircleSpec.ts b/src/spec/vitest/CircleSpec.ts index bcd85937..b2a90ffd 100644 --- a/src/spec/vitest/CircleSpec.ts +++ b/src/spec/vitest/CircleSpec.ts @@ -31,7 +31,7 @@ describe('A Circle Graphic', () => { expect(sut.height).toBe(20); }); - it('can set a color', async () => { + it('@visual can set a color', async () => { const sut = new ex.Circle({ radius: 10, padding: 0, @@ -50,7 +50,7 @@ describe('A Circle Graphic', () => { await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsCircleSpec/circle.png'); }); - it('can set a lineWidth', async () => { + it('@visual can set a lineWidth', async () => { const sut = new ex.Circle({ radius: 30, lineWidth: 15, diff --git a/src/spec/vitest/CollisionShapeSpec.ts b/src/spec/vitest/CollisionShapeSpec.ts index 43d48a04..333ad169 100644 --- a/src/spec/vitest/CollisionShapeSpec.ts +++ b/src/spec/vitest/CollisionShapeSpec.ts @@ -1,7 +1,7 @@ import * as ex from '@excalibur'; import { CollisionJumpTable } from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('Collision Shape', () => { describe('a Circle', () => { @@ -363,7 +363,7 @@ describe('Collision Shape', () => { expect(contact.points[0].y).toBe(0); }); - it('can be debug drawn', async () => { + it('@visual can be debug drawn', async () => { const canvasElement = document.createElement('canvas'); canvasElement.width = 100; canvasElement.height = 100; @@ -385,7 +385,7 @@ describe('Collision Shape', () => { await expect(canvasElement).toEqualImage('/src/spec/assets/images/CollisionShapeSpec/circle-debug.png'); }); - it('can be drawn with actor when in constructor', async () => { + it('@visual can be drawn with actor when in constructor', async () => { const circleActor = new ex.Actor({ pos: new ex.Vector(100, 100), color: ex.Color.Blue, @@ -816,7 +816,7 @@ describe('Collision Shape', () => { expect(tooFar, 'The polygon should be too far away for a hit').toBe(null); }); - it('can be debug drawn', async () => { + it('@visual can be debug drawn', async () => { const polygon = new ex.PolygonCollider({ offset: new ex.Vector(100, 100), points: [new ex.Vector(0, -100), new ex.Vector(-100, 50), new ex.Vector(100, 50)] @@ -832,7 +832,7 @@ describe('Collision Shape', () => { await expect(engine.canvas).toEqualImage('/src/spec/assets/images/CollisionShapeSpec/triangle.png'); }); - it('can be drawn with actor', async () => { + it('@visual can be drawn with actor', async () => { const polygonActor = new ex.Actor({ pos: new ex.Vector(150, 100), collider: ex.Shape.Polygon([new ex.Vector(0, -100), new ex.Vector(-100, 50), new ex.Vector(100, 50)]) @@ -1053,7 +1053,7 @@ describe('Collision Shape', () => { expect(moi).toBeCloseTo(10 * length * length, 0.001); }); - it('can be drawn', async () => { + it('@visual can be drawn', async () => { const edge = new ex.EdgeCollider({ begin: new ex.Vector(100, 100), end: new ex.Vector(400, 400) @@ -1068,7 +1068,7 @@ describe('Collision Shape', () => { await expect(engine.canvas).toEqualImage('/src/spec/assets/images/CollisionShapeSpec/edge.png'); }); - it('can be drawn with actor', async () => { + it('@visual can be drawn with actor', async () => { const edgeActor = new ex.Actor({ pos: new ex.Vector(150, 100), collider: ex.Shape.Edge(ex.Vector.Zero, new ex.Vector(300, 300)) diff --git a/src/spec/vitest/CollisionSpec.ts b/src/spec/vitest/CollisionSpec.ts index 47e09a73..4aae378a 100644 --- a/src/spec/vitest/CollisionSpec.ts +++ b/src/spec/vitest/CollisionSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; import { getDefaultPhysicsConfig } from '../../engine/Collision/PhysicsConfig'; describe('A Collision', () => { diff --git a/src/spec/vitest/ColorBlindCorrectorSpec.ts b/src/spec/vitest/ColorBlindCorrectorSpec.ts index 41746d24..1eca5eab 100644 --- a/src/spec/vitest/ColorBlindCorrectorSpec.ts +++ b/src/spec/vitest/ColorBlindCorrectorSpec.ts @@ -1,133 +1,135 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('A ColorBlindCorrector', () => { let bg: ex.ImageSource; let engine: ex.Engine; let clock: ex.TestClock; - beforeEach(async () => { - engine = TestUtils.engine({ width: 800, height: 200 }, []); - bg = new ex.ImageSource('/src/spec/assets/images/ColorBlindCorrectorSpec/actor.png'); - const loader = new ex.Loader([bg]); - clock = engine.clock as ex.TestClock; - await TestUtils.runToReady(engine, loader); - }); + describe('@visual', () => { + beforeEach(async () => { + engine = TestUtils.engine({ width: 800, height: 200 }, []); + bg = new ex.ImageSource('/src/spec/assets/images/ColorBlindCorrectorSpec/actor.png'); + const loader = new ex.Loader([bg]); + clock = engine.clock as ex.TestClock; + await TestUtils.runToReady(engine, loader); + }); - afterEach(() => { - engine.stop(); - engine.dispose(); - }); + afterEach(() => { + engine.stop(); + engine.dispose(); + }); - it('is normal', () => - new Promise((done) => { - const actor = new ex.Actor(); - actor.graphics.use(bg.toSprite()); - engine.add(actor); + it('is normal', () => + new Promise((done) => { + const actor = new ex.Actor(); + actor.graphics.use(bg.toSprite()); + engine.add(actor); - engine.once('postframe', (ev: ex.PostDrawEvent) => { - expect(engine.canvas) - .toEqualImage('/src/spec/assets/images/ColorBlindCorrectorSpec/normal.png') - .then(() => { - done(); - }); - }); - clock.step(1); - })); + engine.once('postframe', (ev: ex.PostDrawEvent) => { + expect(engine.canvas) + .toEqualImage('/src/spec/assets/images/ColorBlindCorrectorSpec/normal.png') + .then(() => { + done(); + }); + }); + clock.step(1); + })); - it('corrects deuteranopia', () => - new Promise((done) => { - const actor = new ex.Actor(); - actor.graphics.use(bg.toSprite()); - engine.add(actor); - engine.debug.colorBlindMode.correct(ex.ColorBlindnessMode.Deuteranope); - engine.once('postframe', (ev: ex.PostDrawEvent) => { - expect(engine.canvas) - .toEqualImage('/src/spec/assets/images/ColorBlindCorrectorSpec/deuteranope_correct.png') - .then(() => { - done(); - }); - }); - clock.step(1); - })); + it('corrects deuteranopia', () => + new Promise((done) => { + const actor = new ex.Actor(); + actor.graphics.use(bg.toSprite()); + engine.add(actor); + engine.debug.colorBlindMode.correct(ex.ColorBlindnessMode.Deuteranope); + engine.once('postframe', (ev: ex.PostDrawEvent) => { + expect(engine.canvas) + .toEqualImage('/src/spec/assets/images/ColorBlindCorrectorSpec/deuteranope_correct.png') + .then(() => { + done(); + }); + }); + clock.step(1); + })); - it('simulates deuteranopia', () => - new Promise((done) => { - const actor = new ex.Actor(); - actor.graphics.use(bg.toSprite()); - engine.add(actor); - engine.debug.colorBlindMode.simulate(ex.ColorBlindnessMode.Deuteranope); - engine.once('postframe', (ev: ex.PostDrawEvent) => { - expect(engine.canvas) - .toEqualImage('/src/spec/assets/images/ColorBlindCorrectorSpec/deuteranope_simulate.png') - .then(() => { - done(); - }); - }); - clock.step(1); - })); + it('simulates deuteranopia', () => + new Promise((done) => { + const actor = new ex.Actor(); + actor.graphics.use(bg.toSprite()); + engine.add(actor); + engine.debug.colorBlindMode.simulate(ex.ColorBlindnessMode.Deuteranope); + engine.once('postframe', (ev: ex.PostDrawEvent) => { + expect(engine.canvas) + .toEqualImage('/src/spec/assets/images/ColorBlindCorrectorSpec/deuteranope_simulate.png') + .then(() => { + done(); + }); + }); + clock.step(1); + })); - it('corrects protanopia', () => - new Promise((done) => { - const actor = new ex.Actor(); - actor.graphics.use(bg.toSprite()); - engine.add(actor); - engine.debug.colorBlindMode.correct(ex.ColorBlindnessMode.Protanope); - engine.once('postframe', (ev: ex.PostDrawEvent) => { - expect(engine.canvas) - .toEqualImage('/src/spec/assets/images/ColorBlindCorrectorSpec/protanope_correct.png') - .then(() => { - done(); - }); - }); - clock.step(1); - })); + it('corrects protanopia', () => + new Promise((done) => { + const actor = new ex.Actor(); + actor.graphics.use(bg.toSprite()); + engine.add(actor); + engine.debug.colorBlindMode.correct(ex.ColorBlindnessMode.Protanope); + engine.once('postframe', (ev: ex.PostDrawEvent) => { + expect(engine.canvas) + .toEqualImage('/src/spec/assets/images/ColorBlindCorrectorSpec/protanope_correct.png') + .then(() => { + done(); + }); + }); + clock.step(1); + })); - it('simulates protanopia', () => - new Promise((done) => { - const actor = new ex.Actor(); - actor.graphics.use(bg.toSprite()); - engine.add(actor); - engine.debug.colorBlindMode.simulate(ex.ColorBlindnessMode.Protanope); - engine.once('postframe', (ev: ex.PostDrawEvent) => { - expect(engine.canvas) - .toEqualImage('/src/spec/assets/images/ColorBlindCorrectorSpec/protanope_simulate.png') - .then(() => { - done(); - }); - }); - clock.step(1); - })); + it('simulates protanopia', () => + new Promise((done) => { + const actor = new ex.Actor(); + actor.graphics.use(bg.toSprite()); + engine.add(actor); + engine.debug.colorBlindMode.simulate(ex.ColorBlindnessMode.Protanope); + engine.once('postframe', (ev: ex.PostDrawEvent) => { + expect(engine.canvas) + .toEqualImage('/src/spec/assets/images/ColorBlindCorrectorSpec/protanope_simulate.png') + .then(() => { + done(); + }); + }); + clock.step(1); + })); - it('corrects tritanopia', () => - new Promise((done) => { - const actor = new ex.Actor(); - actor.graphics.use(bg.toSprite()); - engine.add(actor); - engine.debug.colorBlindMode.correct(ex.ColorBlindnessMode.Tritanope); - engine.once('postframe', (ev: ex.PostDrawEvent) => { - expect(engine.canvas) - .toEqualImage('/src/spec/assets/images/ColorBlindCorrectorSpec/tritanope_correct.png') - .then(() => { - done(); - }); - }); - clock.step(1); - })); + it('corrects tritanopia', () => + new Promise((done) => { + const actor = new ex.Actor(); + actor.graphics.use(bg.toSprite()); + engine.add(actor); + engine.debug.colorBlindMode.correct(ex.ColorBlindnessMode.Tritanope); + engine.once('postframe', (ev: ex.PostDrawEvent) => { + expect(engine.canvas) + .toEqualImage('/src/spec/assets/images/ColorBlindCorrectorSpec/tritanope_correct.png') + .then(() => { + done(); + }); + }); + clock.step(1); + })); - it('simulates tritanopia', () => - new Promise((done) => { - const actor = new ex.Actor(); - actor.graphics.use(bg.toSprite()); - engine.add(actor); - engine.debug.colorBlindMode.simulate(ex.ColorBlindnessMode.Tritanope); - engine.once('postframe', (ev: ex.PostDrawEvent) => { - expect(engine.canvas) - .toEqualImage('/src/spec/assets/images/ColorBlindCorrectorSpec/tritanope_simulate.png') - .then(() => { - done(); - }); - }); - clock.step(1); - })); + it('simulates tritanopia', () => + new Promise((done) => { + const actor = new ex.Actor(); + actor.graphics.use(bg.toSprite()); + engine.add(actor); + engine.debug.colorBlindMode.simulate(ex.ColorBlindnessMode.Tritanope); + engine.once('postframe', (ev: ex.PostDrawEvent) => { + expect(engine.canvas) + .toEqualImage('/src/spec/assets/images/ColorBlindCorrectorSpec/tritanope_simulate.png') + .then(() => { + done(); + }); + }); + clock.step(1); + })); + }); }); diff --git a/src/spec/vitest/CompositeColliderSpec.ts b/src/spec/vitest/CompositeColliderSpec.ts index 83cd2684..7603da81 100644 --- a/src/spec/vitest/CompositeColliderSpec.ts +++ b/src/spec/vitest/CompositeColliderSpec.ts @@ -252,7 +252,7 @@ describe('A CompositeCollider', () => { expect(compCollider.contains(vec(-101, 0))).toBe(false); }); - it('can be debug drawn', async () => { + it('@visual can be debug drawn', async () => { const canvasElement = document.createElement('canvas'); canvasElement.width = 300; canvasElement.height = 300; diff --git a/src/spec/vitest/CoroutineSpec.ts b/src/spec/vitest/CoroutineSpec.ts index ea75a8de..2210d54f 100644 --- a/src/spec/vitest/CoroutineSpec.ts +++ b/src/spec/vitest/CoroutineSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('A Coroutine', () => { let engine: ex.Engine; diff --git a/src/spec/vitest/CrossFadeSpec.ts b/src/spec/vitest/CrossFadeSpec.ts index b4ef5588..c50d3895 100644 --- a/src/spec/vitest/CrossFadeSpec.ts +++ b/src/spec/vitest/CrossFadeSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('A CrossFade transition', () => { it('exists', () => { @@ -34,7 +34,7 @@ describe('A CrossFade transition', () => { return await future.promise; } - it.skip('can cross fade', async () => { + it.skip('@visual can cross fade', async () => { const engine = TestUtils.engine({ backgroundColor: ex.Color.ExcaliburBlue }); const clock = engine.clock as ex.TestClock; await TestUtils.runToReady(engine); diff --git a/src/spec/vitest/CurveActionSpec.ts b/src/spec/vitest/CurveActionSpec.ts index 7697a673..069b0543 100644 --- a/src/spec/vitest/CurveActionSpec.ts +++ b/src/spec/vitest/CurveActionSpec.ts @@ -1,6 +1,6 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('A actor can curve', () => { let engine: ex.Engine; diff --git a/src/spec/vitest/DebugSpec.ts b/src/spec/vitest/DebugSpec.ts index 6b071715..328190fa 100644 --- a/src/spec/vitest/DebugSpec.ts +++ b/src/spec/vitest/DebugSpec.ts @@ -12,163 +12,165 @@ describe('Debug draw static', () => { ex.Debug.clear(); }); - it('can draw a point', async () => { - const canvas = document.createElement('canvas'); - canvas.width = 10; - canvas.height = 10; - const context = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvas, - backgroundColor: ex.Color.Black + describe('@visual', () => { + it('can draw a point', async () => { + const canvas = document.createElement('canvas'); + canvas.width = 10; + canvas.height = 10; + const context = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvas, + backgroundColor: ex.Color.Black + }); + context.clear(); + + ex.Debug.drawPoint(ex.vec(5, 5), { size: 5, color: ex.Color.ExcaliburBlue }); + + ex.Debug.flush(context); + context.flush(); + + await expect(canvas).toEqualImage('/src/spec/assets/images/DebugSpec/point.png'); + context.dispose(); }); - context.clear(); - ex.Debug.drawPoint(ex.vec(5, 5), { size: 5, color: ex.Color.ExcaliburBlue }); + it('can draw a line', async () => { + const canvas = document.createElement('canvas'); + canvas.width = 10; + canvas.height = 10; + const context = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvas, + backgroundColor: ex.Color.Black + }); + context.clear(); - ex.Debug.flush(context); - context.flush(); + ex.Debug.drawLine(ex.vec(0, 0), ex.vec(10, 10), { color: ex.Color.ExcaliburBlue }); - await expect(canvas).toEqualImage('/src/spec/assets/images/DebugSpec/point.png'); - context.dispose(); - }); + ex.Debug.flush(context); + context.flush(); - it('can draw a line', async () => { - const canvas = document.createElement('canvas'); - canvas.width = 10; - canvas.height = 10; - const context = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvas, - backgroundColor: ex.Color.Black + await expect(canvas).toEqualImage('/src/spec/assets/images/DebugSpec/line.png'); + context.dispose(); }); - context.clear(); - ex.Debug.drawLine(ex.vec(0, 0), ex.vec(10, 10), { color: ex.Color.ExcaliburBlue }); + it('can draw lines', async () => { + const canvas = document.createElement('canvas'); + canvas.width = 10; + canvas.height = 10; + const context = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvas, + backgroundColor: ex.Color.Black + }); + context.clear(); - ex.Debug.flush(context); - context.flush(); + ex.Debug.drawLines([ex.vec(0, 0), ex.vec(10, 10), ex.vec(10, 0), ex.vec(0, 10)], { color: ex.Color.ExcaliburBlue }); - await expect(canvas).toEqualImage('/src/spec/assets/images/DebugSpec/line.png'); - context.dispose(); - }); + ex.Debug.flush(context); + context.flush(); - it('can draw lines', async () => { - const canvas = document.createElement('canvas'); - canvas.width = 10; - canvas.height = 10; - const context = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvas, - backgroundColor: ex.Color.Black + await expect(canvas).toEqualImage('/src/spec/assets/images/DebugSpec/lines.png', 0.99); + context.dispose(); }); - context.clear(); - ex.Debug.drawLines([ex.vec(0, 0), ex.vec(10, 10), ex.vec(10, 0), ex.vec(0, 10)], { color: ex.Color.ExcaliburBlue }); + it.skip('can draw text', async () => { + const canvas = document.createElement('canvas'); + canvas.width = 100; + canvas.height = 100; + const context = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvas, + backgroundColor: ex.Color.Black + }); + context.clear(); - ex.Debug.flush(context); - context.flush(); + ex.Debug.drawText('some text', ex.vec(0, 50)); - await expect(canvas).toEqualImage('/src/spec/assets/images/DebugSpec/lines.png', 0.99); - context.dispose(); - }); + ex.Debug.flush(context); + context.flush(); - it.skip('can draw text', async () => { - const canvas = document.createElement('canvas'); - canvas.width = 100; - canvas.height = 100; - const context = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvas, - backgroundColor: ex.Color.Black + await expect(canvas).toEqualImage('/src/spec/assets/images/DebugSpec/text.png'); }); - context.clear(); - ex.Debug.drawText('some text', ex.vec(0, 50)); + it('can draw a polygon', async () => { + const canvas = document.createElement('canvas'); + canvas.width = 10; + canvas.height = 10; + const context = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvas, + backgroundColor: ex.Color.Black + }); + context.clear(); - ex.Debug.flush(context); - context.flush(); + ex.Debug.drawPolygon([ex.vec(2, 2), ex.vec(8, 2), ex.vec(8, 8), ex.vec(2, 8)], { color: ex.Color.ExcaliburBlue }); - await expect(canvas).toEqualImage('/src/spec/assets/images/DebugSpec/text.png'); - }); + ex.Debug.flush(context); + context.flush(); - it('can draw a polygon', async () => { - const canvas = document.createElement('canvas'); - canvas.width = 10; - canvas.height = 10; - const context = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvas, - backgroundColor: ex.Color.Black + await expect(canvas).toEqualImage('/src/spec/assets/images/DebugSpec/polygon.png'); + context.dispose(); }); - context.clear(); - ex.Debug.drawPolygon([ex.vec(2, 2), ex.vec(8, 2), ex.vec(8, 8), ex.vec(2, 8)], { color: ex.Color.ExcaliburBlue }); + it('can draw a circle', async () => { + const canvas = document.createElement('canvas'); + canvas.width = 10; + canvas.height = 10; + const context = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvas, + backgroundColor: ex.Color.Black + }); + context.clear(); - ex.Debug.flush(context); - context.flush(); + ex.Debug.drawCircle(ex.vec(5, 5), 5, { color: ex.Color.Transparent, strokeColor: ex.Color.ExcaliburBlue, width: 1 }); - await expect(canvas).toEqualImage('/src/spec/assets/images/DebugSpec/polygon.png'); - context.dispose(); - }); + ex.Debug.flush(context); + context.flush(); - it('can draw a circle', async () => { - const canvas = document.createElement('canvas'); - canvas.width = 10; - canvas.height = 10; - const context = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvas, - backgroundColor: ex.Color.Black + await expect(canvas).toEqualImage('/src/spec/assets/images/DebugSpec/circle.png'); + context.dispose(); }); - context.clear(); - - ex.Debug.drawCircle(ex.vec(5, 5), 5, { color: ex.Color.Transparent, strokeColor: ex.Color.ExcaliburBlue, width: 1 }); - ex.Debug.flush(context); - context.flush(); - - await expect(canvas).toEqualImage('/src/spec/assets/images/DebugSpec/circle.png'); - context.dispose(); - }); - - it('can draw bounds', async () => { - const canvas = document.createElement('canvas'); - canvas.width = 10; - canvas.height = 10; - const context = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvas, - backgroundColor: ex.Color.Black + it('can draw bounds', async () => { + const canvas = document.createElement('canvas'); + canvas.width = 10; + canvas.height = 10; + const context = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvas, + backgroundColor: ex.Color.Black + }); + context.clear(); + + const bounds = new ex.BoundingBox({ + left: 3, + right: 7, + top: 3, + bottom: 7 + }); + + ex.Debug.drawBounds(bounds, { color: ex.Color.ExcaliburBlue }); + + ex.Debug.flush(context); + context.flush(); + + await expect(canvas).toEqualImage('/src/spec/assets/images/DebugSpec/bounds.png'); + context.dispose(); }); - context.clear(); - const bounds = new ex.BoundingBox({ - left: 3, - right: 7, - top: 3, - bottom: 7 - }); + it('can draw a ray', async () => { + const canvas = document.createElement('canvas'); + canvas.width = 10; + canvas.height = 10; + const context = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvas, + backgroundColor: ex.Color.Black + }); + context.clear(); - ex.Debug.drawBounds(bounds, { color: ex.Color.ExcaliburBlue }); + const ray = new ex.Ray(ex.vec(0, 0), ex.vec(1, 1)); - ex.Debug.flush(context); - context.flush(); + ex.Debug.drawRay(ray, { distance: 4, color: ex.Color.ExcaliburBlue }); - await expect(canvas).toEqualImage('/src/spec/assets/images/DebugSpec/bounds.png'); - context.dispose(); - }); + ex.Debug.flush(context); + context.flush(); - it('can draw a ray', async () => { - const canvas = document.createElement('canvas'); - canvas.width = 10; - canvas.height = 10; - const context = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvas, - backgroundColor: ex.Color.Black + await expect(canvas).toEqualImage('/src/spec/assets/images/DebugSpec/ray.png'); + context.dispose(); }); - context.clear(); - - const ray = new ex.Ray(ex.vec(0, 0), ex.vec(1, 1)); - - ex.Debug.drawRay(ray, { distance: 4, color: ex.Color.ExcaliburBlue }); - - ex.Debug.flush(context); - context.flush(); - - await expect(canvas).toEqualImage('/src/spec/assets/images/DebugSpec/ray.png'); - context.dispose(); }); }); diff --git a/src/spec/vitest/DebugSystemSpec.ts b/src/spec/vitest/DebugSystemSpec.ts index e560fff4..4aceafc5 100644 --- a/src/spec/vitest/DebugSystemSpec.ts +++ b/src/spec/vitest/DebugSystemSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('DebugSystem', () => { let engine: ex.Engine; @@ -54,185 +54,187 @@ describe('DebugSystem', () => { }).not.toThrow(); }); - it('can show transform and entity info', async () => { - const world = engine.currentScene.world; - const debugSystem = new ex.DebugSystem(world); - engine.currentScene.world.add(debugSystem); - debugSystem.initialize(world, engine.currentScene); - - engine.graphicsContext.clear(); - await (engine.graphicsContext.debug as any)._debugText.load(); - - const actor = new ex.Actor({ name: 'thingy', x: center.x, y: center.y, width: 50, height: 50, color: ex.Color.Yellow }); - actor.id = 0; - engine.debug.transform.showAll = true; - engine.debug.entity.showAll = true; - debugSystem.query.checkAndAdd(actor); - debugSystem.update(); - - engine.graphicsContext.flush(); - - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/DebugSystemSpec/transform.png'); - }); - - it('can show motion info', async () => { - const world = engine.currentScene.world; - const debugSystem = new ex.DebugSystem(world); - engine.currentScene.world.add(debugSystem); - debugSystem.initialize(world, engine.currentScene); - - engine.graphicsContext.clear(); - await (engine.graphicsContext.debug as any)._debugText.load(); - - const actor = new ex.Actor({ name: 'thingy', x: center.x, y: center.y, width: 50, height: 50, color: ex.Color.Yellow }); - actor.id = 0; - actor.vel = ex.vec(100, 0); - actor.acc = ex.vec(100, -100); - debugSystem.query.checkAndAdd(actor); - engine.debug.motion.showAll = true; - engine.debug.collider.showGeometry = true; - engine.debug.collider.geometryLineWidth = 2; - debugSystem.update(); - - engine.graphicsContext.flush(); - - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/DebugSystemSpec/motion.png'); - }); - - it('can show body info', async () => { - const world = engine.currentScene.world; - const debugSystem = new ex.DebugSystem(world); - engine.currentScene.world.add(debugSystem); - debugSystem.initialize(world, engine.currentScene); - - engine.graphicsContext.clear(); - await (engine.graphicsContext.debug as any)._debugText.load(); - - const actor = new ex.Actor({ name: 'thingy', x: -100 + center.x, y: center.y, width: 50, height: 50, color: ex.Color.Yellow }); - actor.id = 0; - actor.vel = ex.vec(100, 0); - actor.acc = ex.vec(100, -100); - debugSystem.query.checkAndAdd(actor); - engine.debug.body.showAll = true; - debugSystem.update(); + describe('@visual', () => { + it('can show transform and entity info', async () => { + const world = engine.currentScene.world; + const debugSystem = new ex.DebugSystem(world); + engine.currentScene.world.add(debugSystem); + debugSystem.initialize(world, engine.currentScene); + + engine.graphicsContext.clear(); + await (engine.graphicsContext.debug as any)._debugText.load(); + + const actor = new ex.Actor({ name: 'thingy', x: center.x, y: center.y, width: 50, height: 50, color: ex.Color.Yellow }); + actor.id = 0; + engine.debug.transform.showAll = true; + engine.debug.entity.showAll = true; + debugSystem.query.checkAndAdd(actor); + debugSystem.update(); - engine.graphicsContext.flush(); + engine.graphicsContext.flush(); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/DebugSystemSpec/body.png'); - }); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/DebugSystemSpec/transform.png'); + }); - it('can show collider info', async () => { - const world = engine.currentScene.world; - const debugSystem = new ex.DebugSystem(world); - engine.currentScene.world.add(debugSystem); - debugSystem.initialize(world, engine.currentScene); + it('can show motion info', async () => { + const world = engine.currentScene.world; + const debugSystem = new ex.DebugSystem(world); + engine.currentScene.world.add(debugSystem); + debugSystem.initialize(world, engine.currentScene); + + engine.graphicsContext.clear(); + await (engine.graphicsContext.debug as any)._debugText.load(); + + const actor = new ex.Actor({ name: 'thingy', x: center.x, y: center.y, width: 50, height: 50, color: ex.Color.Yellow }); + actor.id = 0; + actor.vel = ex.vec(100, 0); + actor.acc = ex.vec(100, -100); + debugSystem.query.checkAndAdd(actor); + engine.debug.motion.showAll = true; + engine.debug.collider.showGeometry = true; + engine.debug.collider.geometryLineWidth = 2; + debugSystem.update(); - engine.graphicsContext.clear(); - await (engine.graphicsContext.debug as any)._debugText.load(); + engine.graphicsContext.flush(); - const actor = new ex.Actor({ name: 'thingy', x: -100 + center.x, y: center.y, width: 50, height: 50, color: ex.Color.Yellow }); - actor.id = 0; - debugSystem.query.checkAndAdd(actor); - engine.debug.entity.showId = true; - engine.debug.collider.showAll = true; - debugSystem.update(); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/DebugSystemSpec/motion.png'); + }); - engine.graphicsContext.flush(); + it('can show body info', async () => { + const world = engine.currentScene.world; + const debugSystem = new ex.DebugSystem(world); + engine.currentScene.world.add(debugSystem); + debugSystem.initialize(world, engine.currentScene); + + engine.graphicsContext.clear(); + await (engine.graphicsContext.debug as any)._debugText.load(); + + const actor = new ex.Actor({ name: 'thingy', x: -100 + center.x, y: center.y, width: 50, height: 50, color: ex.Color.Yellow }); + actor.id = 0; + actor.vel = ex.vec(100, 0); + actor.acc = ex.vec(100, -100); + debugSystem.query.checkAndAdd(actor); + engine.debug.body.showAll = true; + debugSystem.update(); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/DebugSystemSpec/collider.png'); - }); + engine.graphicsContext.flush(); - it('can show composite collider info', async () => { - const world = engine.currentScene.world; - const debugSystem = new ex.DebugSystem(world); - engine.currentScene.world.add(debugSystem); - debugSystem.initialize(world, engine.currentScene); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/DebugSystemSpec/body.png'); + }); - engine.graphicsContext.clear(); - await (engine.graphicsContext.debug as any)._debugText.load(); + it('can show collider info', async () => { + const world = engine.currentScene.world; + const debugSystem = new ex.DebugSystem(world); + engine.currentScene.world.add(debugSystem); + debugSystem.initialize(world, engine.currentScene); - const actor = new ex.Actor({ name: 'thingy', x: -100 + center.x, y: center.y, width: 50, height: 50, color: ex.Color.Yellow }); - actor.collider.useCompositeCollider([ex.Shape.Circle(50), ex.Shape.Box(150, 20), ex.Shape.Box(10, 150)]); - actor.id = 0; - debugSystem.query.checkAndAdd(actor); - engine.debug.collider.showAll = true; - engine.debug.collider.geometryLineWidth = 3; - debugSystem.update(); + engine.graphicsContext.clear(); + await (engine.graphicsContext.debug as any)._debugText.load(); - engine.graphicsContext.flush(); + const actor = new ex.Actor({ name: 'thingy', x: -100 + center.x, y: center.y, width: 50, height: 50, color: ex.Color.Yellow }); + actor.id = 0; + debugSystem.query.checkAndAdd(actor); + engine.debug.entity.showId = true; + engine.debug.collider.showAll = true; + debugSystem.update(); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/DebugSystemSpec/composite-collider.png'); - }); + engine.graphicsContext.flush(); - it('can show graphics info', async () => { - const world = engine.currentScene.world; - const debugSystem = new ex.DebugSystem(world); - engine.currentScene.world.add(debugSystem); - debugSystem.initialize(world, engine.currentScene); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/DebugSystemSpec/collider.png'); + }); - engine.graphicsContext.clear(); - await (engine.graphicsContext.debug as any)._debugText.load(); + it('can show composite collider info', async () => { + const world = engine.currentScene.world; + const debugSystem = new ex.DebugSystem(world); + engine.currentScene.world.add(debugSystem); + debugSystem.initialize(world, engine.currentScene); + + engine.graphicsContext.clear(); + await (engine.graphicsContext.debug as any)._debugText.load(); + + const actor = new ex.Actor({ name: 'thingy', x: -100 + center.x, y: center.y, width: 50, height: 50, color: ex.Color.Yellow }); + actor.collider.useCompositeCollider([ex.Shape.Circle(50), ex.Shape.Box(150, 20), ex.Shape.Box(10, 150)]); + actor.id = 0; + debugSystem.query.checkAndAdd(actor); + engine.debug.collider.showAll = true; + engine.debug.collider.geometryLineWidth = 3; + debugSystem.update(); - const actor = new ex.Actor({ name: 'thingy', x: -100 + center.x, y: center.y, width: 50, height: 50 }); - actor.graphics.use(new ex.Rectangle({ width: 200, height: 100, color: ex.Color.Red })); - actor.id = 0; - debugSystem.query.checkAndAdd(actor); - engine.debug.collider.showBounds = false; - engine.debug.collider.showGeometry = false; - engine.debug.collider.showOwner = false; - engine.debug.graphics.showAll = true; - debugSystem.update(); + engine.graphicsContext.flush(); - engine.graphicsContext.flush(); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/DebugSystemSpec/composite-collider.png'); + }); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/DebugSystemSpec/graphics.png'); - }); + it('can show graphics info', async () => { + const world = engine.currentScene.world; + const debugSystem = new ex.DebugSystem(world); + engine.currentScene.world.add(debugSystem); + debugSystem.initialize(world, engine.currentScene); + + engine.graphicsContext.clear(); + await (engine.graphicsContext.debug as any)._debugText.load(); + + const actor = new ex.Actor({ name: 'thingy', x: -100 + center.x, y: center.y, width: 50, height: 50 }); + actor.graphics.use(new ex.Rectangle({ width: 200, height: 100, color: ex.Color.Red })); + actor.id = 0; + debugSystem.query.checkAndAdd(actor); + engine.debug.collider.showBounds = false; + engine.debug.collider.showGeometry = false; + engine.debug.collider.showOwner = false; + engine.debug.graphics.showAll = true; + debugSystem.update(); - it('can show DebugGraphicsComponent', async () => { - const world = engine.currentScene.world; - const debugSystem = new ex.DebugSystem(world); - engine.currentScene.world.add(debugSystem); - debugSystem.initialize(world, engine.currentScene); + engine.graphicsContext.flush(); - engine.graphicsContext.clear(); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/DebugSystemSpec/graphics.png'); + }); - const entity = new ex.Entity([ - new ex.TransformComponent(), - new ex.DebugGraphicsComponent((ctx) => { - ctx.drawCircle(ex.vec(250, 250), 100, ex.Color.Blue); - }) - ]); - debugSystem.query.checkAndAdd(entity); - debugSystem.update(); + it('can show DebugGraphicsComponent', async () => { + const world = engine.currentScene.world; + const debugSystem = new ex.DebugSystem(world); + engine.currentScene.world.add(debugSystem); + debugSystem.initialize(world, engine.currentScene); + + engine.graphicsContext.clear(); + + const entity = new ex.Entity([ + new ex.TransformComponent(), + new ex.DebugGraphicsComponent((ctx) => { + ctx.drawCircle(ex.vec(250, 250), 100, ex.Color.Blue); + }) + ]); + debugSystem.query.checkAndAdd(entity); + debugSystem.update(); - engine.graphicsContext.flush(); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/DebugSystemSpec/debug-draw-component.png'); - }); + engine.graphicsContext.flush(); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/DebugSystemSpec/debug-draw-component.png'); + }); - it('can debug draw a tilemap', async () => { - const world = engine.currentScene.world; - const debugSystem = new ex.DebugSystem(world); - engine.currentScene.world.add(debugSystem); - debugSystem.initialize(world, engine.currentScene); + it('can debug draw a tilemap', async () => { + const world = engine.currentScene.world; + const debugSystem = new ex.DebugSystem(world); + engine.currentScene.world.add(debugSystem); + debugSystem.initialize(world, engine.currentScene); + + engine.graphicsContext.clear(); + engine.debug.tilemap.showGrid = true; + engine.debug.tilemap.showSolidBounds = true; + engine.debug.tilemap.showColliderGeometry = true; + + const tilemap = new ex.TileMap({ + pos: ex.vec(0, 0), + tileWidth: 50, + tileHeight: 50, + rows: 10, + columns: 10 + }); + tilemap.tiles[0].solid = true; + tilemap.update(engine, 1); + debugSystem.query.checkAndAdd(tilemap); + debugSystem.update(); - engine.graphicsContext.clear(); - engine.debug.tilemap.showGrid = true; - engine.debug.tilemap.showSolidBounds = true; - engine.debug.tilemap.showColliderGeometry = true; - - const tilemap = new ex.TileMap({ - pos: ex.vec(0, 0), - tileWidth: 50, - tileHeight: 50, - rows: 10, - columns: 10 + engine.graphicsContext.flush(); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/DebugSystemSpec/tilemap-debug.png'); }); - tilemap.tiles[0].solid = true; - tilemap.update(engine, 1); - debugSystem.query.checkAndAdd(tilemap); - debugSystem.update(); - - engine.graphicsContext.flush(); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/DebugSystemSpec/tilemap-debug.png'); }); }); diff --git a/src/spec/vitest/DebugTextSpec.ts b/src/spec/vitest/DebugTextSpec.ts index fcf74ae7..1cb048cd 100644 --- a/src/spec/vitest/DebugTextSpec.ts +++ b/src/spec/vitest/DebugTextSpec.ts @@ -5,41 +5,43 @@ describe('DebugText', () => { expect(ex.DebugText); }); - it('can write text (2DCanvas)', async () => { - const canvasElement = document.createElement('canvas'); - canvasElement.width = 100; - canvasElement.height = 100; - const ctx = new ex.ExcaliburGraphicsContext2DCanvas({ canvasElement }); + describe('@visual', () => { + it('can write text (2DCanvas)', async () => { + const canvasElement = document.createElement('canvas'); + canvasElement.width = 100; + canvasElement.height = 100; + const ctx = new ex.ExcaliburGraphicsContext2DCanvas({ canvasElement }); - const debugText = new ex.DebugText(); + const debugText = new ex.DebugText(); - await debugText.load(); + await debugText.load(); - ctx.clear(); + ctx.clear(); - debugText.write(ctx, 'some text', ex.vec(0, 50)); + debugText.write(ctx, 'some text', ex.vec(0, 50)); - ctx.flush(); + ctx.flush(); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/DebugTextSpec/draw-canvas2d.png'); - }); + await expect(canvasElement).toEqualImage('/src/spec/assets/images/DebugTextSpec/draw-canvas2d.png'); + }); - it('can write text (WebGL)', async () => { - const canvasElement = document.createElement('canvas'); - canvasElement.width = 100; - canvasElement.height = 100; - const ctx = new ex.ExcaliburGraphicsContextWebGL({ canvasElement }); + it('can write text (WebGL)', async () => { + const canvasElement = document.createElement('canvas'); + canvasElement.width = 100; + canvasElement.height = 100; + const ctx = new ex.ExcaliburGraphicsContextWebGL({ canvasElement }); - const debugText = new ex.DebugText(); + const debugText = new ex.DebugText(); - await debugText.load(); + await debugText.load(); - ctx.clear(); + ctx.clear(); - debugText.write(ctx, 'some text', ex.vec(0, 50)); + debugText.write(ctx, 'some text', ex.vec(0, 50)); - ctx.flush(); + ctx.flush(); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/DebugTextSpec/draw-webgl.png', 0.94); + await expect(canvasElement).toEqualImage('/src/spec/assets/images/DebugTextSpec/draw-webgl.png', 0.94); + }); }); }); diff --git a/src/spec/vitest/DefaultLoaderSpec.ts b/src/spec/vitest/DefaultLoaderSpec.ts index adccfc9a..2fc4892c 100644 --- a/src/spec/vitest/DefaultLoaderSpec.ts +++ b/src/spec/vitest/DefaultLoaderSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('A DefaultLoader', () => { let engine: ex.Engine; @@ -37,7 +37,7 @@ describe('A DefaultLoader', () => { expect(sut.progress).toBe(0); }); - it('can draw', async () => { + it('@visual can draw', async () => { const sut = new ex.DefaultLoader({ loadables: [, , ,] }); diff --git a/src/spec/vitest/DirectorSpec.ts b/src/spec/vitest/DirectorSpec.ts index 3aaec26e..6b6295f1 100644 --- a/src/spec/vitest/DirectorSpec.ts +++ b/src/spec/vitest/DirectorSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('A Director', () => { it('exists', () => { @@ -122,7 +122,7 @@ describe('A Director', () => { expect(sut.getSceneName(new ex.Scene())).toBe(null); }); - it('will draw a start scene transition', async () => { + it('@visual will draw a start scene transition', async () => { const engine = TestUtils.engine(); const clock = engine.clock as ex.TestClock; clock.start(); diff --git a/src/spec/vitest/EngineSpec.ts b/src/spec/vitest/EngineSpec.ts index dfa5dae7..b3b989c0 100644 --- a/src/spec/vitest/EngineSpec.ts +++ b/src/spec/vitest/EngineSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; import { inject } from 'vitest'; /** @@ -60,7 +60,7 @@ describe('The engine', () => { expect(exceptionSpy).not.toHaveBeenCalled(); }); - it.skip('should show the play button by default', () => + it.skip('@visual should show the play button by default', () => new Promise((done) => { reset(); engine = TestUtils.engine({ @@ -229,7 +229,7 @@ describe('The engine', () => { }); it('should have a default resolution to SVGA (800x600) if none specified', () => { - const engine = new ex.Engine(); + const engine = new ex.Engine({ suppressConsoleBootMessage: true }); expect(engine.screen.displayMode).toBe(ex.DisplayMode.FitScreen); expect(engine.screen.resolution.width).toBe(800); expect(engine.screen.resolution.height).toBe(600); @@ -254,7 +254,7 @@ describe('The engine', () => { expect(ex.TextureLoader.filtering).toBe(ex.ImageFiltering.Pixel); }); - it('should not show the play button when suppressPlayButton is turned on', () => + it('@visual should not show the play button when suppressPlayButton is turned on', () => new Promise((done) => { reset(); engine = TestUtils.engine({ @@ -744,10 +744,44 @@ describe('The engine', () => { engine.dispose(); }); - it('can screen shot the game (in WebGL)', () => - new Promise((done) => { + describe('@visual', () => { + it('can screen shot the game (in WebGL)', () => + new Promise((done) => { + const engine = TestUtils.engine({}, []); + const clock = engine.clock as ex.TestClock; + + engine.add( + new ex.Actor({ + x: 50, + y: 50, + width: 100, + height: 100, + color: ex.Color.Red + }) + ); + TestUtils.runToReady(engine).then(() => { + clock.step(1); + + const canvas = flushWebGLCanvasTo2D(engine.canvas); + engine.screenshot().then((image) => { + expect(image) + .toEqualImage(canvas) + .then(() => { + done(); + engine.dispose(); + }); + }); + clock.step(1); + clock.step(1); + clock.step(1); + }); + })); + + it('can screen shot the game HiDPI (in WebGL)', async () => { const engine = TestUtils.engine({}, []); const clock = engine.clock as ex.TestClock; + (engine.screen as any)._pixelRatioOverride = 2; + engine.screen.applyResolutionAndViewport(); engine.add( new ex.Actor({ @@ -758,215 +792,183 @@ describe('The engine', () => { color: ex.Color.Red }) ); - TestUtils.runToReady(engine).then(() => { - clock.step(1); - - const canvas = flushWebGLCanvasTo2D(engine.canvas); - engine.screenshot().then((image) => { - expect(image) - .toEqualImage(canvas) - .then(() => { - done(); - engine.dispose(); - }); - }); - clock.step(1); - clock.step(1); - clock.step(1); - }); - })); + await TestUtils.runToReady(engine); - it('can screen shot the game HiDPI (in WebGL)', async () => { - const engine = TestUtils.engine({}, []); - const clock = engine.clock as ex.TestClock; - (engine.screen as any)._pixelRatioOverride = 2; - engine.screen.applyResolutionAndViewport(); + clock.step(1); + const screenShotPromise = engine.screenshot(); + clock.step(1); + const canvas = flushWebGLCanvasTo2D(engine.canvas); + const hidpiImagePromise = engine.screenshot(true); + clock.step(1); - engine.add( - new ex.Actor({ - x: 50, - y: 50, - width: 100, - height: 100, - color: ex.Color.Red - }) - ); - await TestUtils.runToReady(engine); + const image = await screenShotPromise; + const hidpiImage = await hidpiImagePromise; - clock.step(1); - const screenShotPromise = engine.screenshot(); - clock.step(1); - const canvas = flushWebGLCanvasTo2D(engine.canvas); - const hidpiImagePromise = engine.screenshot(true); - clock.step(1); + expect(image.width).toBe(500); + expect(image.height).toBe(500); + + expect(hidpiImage.width).toBe(1000); + expect(hidpiImage.height).toBe(1000); + await expect(hidpiImage).toEqualImage(canvas); + engine.dispose(); + }); - const image = await screenShotPromise; - const hidpiImage = await hidpiImagePromise; + it('can screen shot and match the anti-aliasing with a half pixel when pixelRatio != 1.0', async ({ skip }) => { + // safari inconsistencies + if (inject('browser') === 'webkit') { + skip(); + } - expect(image.width).toBe(500); - expect(image.height).toBe(500); + const engine = TestUtils.engine( + { + width: 200, + height: 200, + pixelRatio: 1.2, + suppressHiDPIScaling: false, + antialiasing: false, + snapToPixel: false + }, + [] + ); + const clock = engine.clock as ex.TestClock; + const img = new ex.ImageSource('/src/spec/assets/images/EngineSpec/sprite.png'); + await img.load(); + const actor = new ex.Actor({ + x: 40.5, + y: 40.0, + scale: ex.vec(2, 2) + }); + actor.graphics.use(img.toSprite()); + engine.add(actor); + await TestUtils.runToReady(engine); - expect(hidpiImage.width).toBe(1000); - expect(hidpiImage.height).toBe(1000); - await expect(hidpiImage).toEqualImage(canvas); - engine.dispose(); - }); + clock.step(1); + const screenShotPromise = engine.screenshot(); + clock.step(1); - it('can screen shot and match the anti-aliasing with a half pixel when pixelRatio != 1.0', async ({ skip }) => { - // safari inconsistencies - if (inject('browser') === 'webkit') { - skip(); - } + const image = await screenShotPromise; - const engine = TestUtils.engine( - { - width: 200, - height: 200, - pixelRatio: 1.2, - suppressHiDPIScaling: false, - antialiasing: false, - snapToPixel: false - }, - [] - ); - const clock = engine.clock as ex.TestClock; - const img = new ex.ImageSource('/src/spec/assets/images/EngineSpec/sprite.png'); - await img.load(); - const actor = new ex.Actor({ - x: 40.5, - y: 40.0, - scale: ex.vec(2, 2) + await expect(image).toEqualImage('/src/spec/assets/images/EngineSpec/screenshot.png', 0.999); + engine.dispose(); }); - actor.graphics.use(img.toSprite()); - engine.add(actor); - await TestUtils.runToReady(engine); - clock.step(1); - const screenShotPromise = engine.screenshot(); - clock.step(1); + it('can snap to pixel', async () => { + const engine = TestUtils.engine({ + snapToPixel: true, + antialiasing: false, + width: 256, + height: 256 + }); + const clock = engine.clock as ex.TestClock; + await TestUtils.runToReady(engine); + const playerImage = new ex.ImageSource('/src/spec/assets/images/EngineSpec/hero.png'); + const playerSpriteSheet = ex.SpriteSheet.fromImageSource({ + image: playerImage, + grid: { + spriteWidth: 16, + spriteHeight: 16, + rows: 8, + columns: 8 + } + }); + const backgroundImage = new ex.ImageSource('/src/spec/assets/images/EngineSpec/tileset.png'); + const backgroundSpriteSheet = ex.SpriteSheet.fromImageSource({ + image: backgroundImage, + grid: { + spriteHeight: 16, + spriteWidth: 16, + columns: 27, + rows: 15 + } + }); + await playerImage.load(); + await backgroundImage.load(); + + const tilemap = new ex.TileMap({ + tileWidth: 16, + tileHeight: 16, + rows: 20, + columns: 20, + pos: ex.vec(0, 0) + }); + tilemap.tiles.forEach((t) => { + t.addGraphic(backgroundSpriteSheet.getSprite(6, 0)); + }); - const image = await screenShotPromise; + const player = new ex.Actor({ + anchor: ex.vec(0, 0), + pos: ex.vec(16 * 8 + 0.9, 16 * 8 + 0.9) + }); + player.graphics.use(playerSpriteSheet.getSprite(0, 0)); - await expect(image).toEqualImage('/src/spec/assets/images/EngineSpec/screenshot.png', 0.999); - engine.dispose(); - }); + engine.add(tilemap); + engine.add(player); + engine.currentScene.camera.pos = player.pos.add(ex.vec(0.05, 0.05)); - it('can snap to pixel', async () => { - const engine = TestUtils.engine({ - snapToPixel: true, - antialiasing: false, - width: 256, - height: 256 - }); - const clock = engine.clock as ex.TestClock; - await TestUtils.runToReady(engine); - const playerImage = new ex.ImageSource('/src/spec/assets/images/EngineSpec/hero.png'); - const playerSpriteSheet = ex.SpriteSheet.fromImageSource({ - image: playerImage, - grid: { - spriteWidth: 16, - spriteHeight: 16, - rows: 8, - columns: 8 - } - }); - const backgroundImage = new ex.ImageSource('/src/spec/assets/images/EngineSpec/tileset.png'); - const backgroundSpriteSheet = ex.SpriteSheet.fromImageSource({ - image: backgroundImage, - grid: { - spriteHeight: 16, - spriteWidth: 16, - columns: 27, - rows: 15 - } - }); - await playerImage.load(); - await backgroundImage.load(); - - const tilemap = new ex.TileMap({ - tileWidth: 16, - tileHeight: 16, - rows: 20, - columns: 20, - pos: ex.vec(0, 0) - }); - tilemap.tiles.forEach((t) => { - t.addGraphic(backgroundSpriteSheet.getSprite(6, 0)); - }); + clock.step(); - const player = new ex.Actor({ - anchor: ex.vec(0, 0), - pos: ex.vec(16 * 8 + 0.9, 16 * 8 + 0.9) + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/EngineSpec/snaptopixel.png'); + engine.dispose(); }); - player.graphics.use(playerSpriteSheet.getSprite(0, 0)); - engine.add(tilemap); - engine.add(player); - engine.currentScene.camera.pos = player.pos.add(ex.vec(0.05, 0.05)); - - clock.step(); + it('can do subpixel AA on pixel art', async () => { + const engine = TestUtils.engine({ + pixelArt: true, + width: 256, + height: 256, + suppressHiDPIScaling: false, + pixelRatio: 2 + }); + const clock = engine.clock as ex.TestClock; + await TestUtils.runToReady(engine); + const playerImage = new ex.ImageSource('/src/spec/assets/images/EngineSpec/hero.png'); + const playerSpriteSheet = ex.SpriteSheet.fromImageSource({ + image: playerImage, + grid: { + spriteWidth: 16, + spriteHeight: 16, + rows: 8, + columns: 8 + } + }); + const backgroundImage = new ex.ImageSource('/src/spec/assets/images/EngineSpec/tileset.png'); + const backgroundSpriteSheet = ex.SpriteSheet.fromImageSource({ + image: backgroundImage, + grid: { + spriteHeight: 16, + spriteWidth: 16, + columns: 27, + rows: 15 + } + }); + await playerImage.load(); + await backgroundImage.load(); + + const tilemap = new ex.TileMap({ + tileWidth: 16, + tileHeight: 16, + rows: 20, + columns: 20, + pos: ex.vec(0, 0) + }); + tilemap.tiles.forEach((t) => { + t.addGraphic(backgroundSpriteSheet.getSprite(6, 0)); + }); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/EngineSpec/snaptopixel.png'); - engine.dispose(); - }); + const player = new ex.Actor({ + anchor: ex.vec(0, 0), + pos: ex.vec(127.45599999999973, 117.3119999999999) + }); + player.graphics.use(playerSpriteSheet.getSprite(0, 0)); - it('can do subpixel AA on pixel art', async () => { - const engine = TestUtils.engine({ - pixelArt: true, - width: 256, - height: 256, - suppressHiDPIScaling: false, - pixelRatio: 2 - }); - const clock = engine.clock as ex.TestClock; - await TestUtils.runToReady(engine); - const playerImage = new ex.ImageSource('/src/spec/assets/images/EngineSpec/hero.png'); - const playerSpriteSheet = ex.SpriteSheet.fromImageSource({ - image: playerImage, - grid: { - spriteWidth: 16, - spriteHeight: 16, - rows: 8, - columns: 8 - } - }); - const backgroundImage = new ex.ImageSource('/src/spec/assets/images/EngineSpec/tileset.png'); - const backgroundSpriteSheet = ex.SpriteSheet.fromImageSource({ - image: backgroundImage, - grid: { - spriteHeight: 16, - spriteWidth: 16, - columns: 27, - rows: 15 - } - }); - await playerImage.load(); - await backgroundImage.load(); - - const tilemap = new ex.TileMap({ - tileWidth: 16, - tileHeight: 16, - rows: 20, - columns: 20, - pos: ex.vec(0, 0) - }); - tilemap.tiles.forEach((t) => { - t.addGraphic(backgroundSpriteSheet.getSprite(6, 0)); - }); + engine.add(tilemap); + engine.add(player); + engine.currentScene.camera.pos = player.pos; - const player = new ex.Actor({ - anchor: ex.vec(0, 0), - pos: ex.vec(127.45599999999973, 117.3119999999999) + clock.step(); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/EngineSpec/pixelart.png'); + engine.dispose(); }); - player.graphics.use(playerSpriteSheet.getSprite(0, 0)); - - engine.add(tilemap); - engine.add(player); - engine.currentScene.camera.pos = player.pos; - - clock.step(); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/EngineSpec/pixelart.png'); - engine.dispose(); }); describe('lifecycle overrides', () => { diff --git a/src/spec/vitest/ExcaliburGraphicsContextSpec.ts b/src/spec/vitest/ExcaliburGraphicsContextSpec.ts index 1cd65a97..e191e36f 100644 --- a/src/spec/vitest/ExcaliburGraphicsContextSpec.ts +++ b/src/spec/vitest/ExcaliburGraphicsContextSpec.ts @@ -44,166 +44,168 @@ describe('The ExcaliburGraphicsContext', () => { expect(context.height).toBe(canvas.height); }); - it('can draw a graphic', async () => { - const canvasElement = testCanvasElement; - canvasElement.width = 100; - canvasElement.height = 100; - const sut = new ex.ExcaliburGraphicsContext2DCanvas({ - canvasElement, - enableTransparency: false, - backgroundColor: ex.Color.White - }); - - const rect = new ex.Rectangle({ - width: 50, - height: 50, - color: ex.Color.Blue - }); - - sut.clear(); - sut.drawImage(rect._bitmap, 20, 20); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/2d-drawgraphic.png'); - }); - - it('can draw debug point', async () => { - const canvasElement = testCanvasElement; - canvasElement.width = 100; - canvasElement.height = 100; - const sut = new ex.ExcaliburGraphicsContext2DCanvas({ - canvasElement, - enableTransparency: false, - backgroundColor: ex.Color.White - }); - - sut.clear(); - sut.debug.drawPoint(ex.vec(50, 50), { - size: 20, - color: ex.Color.Blue - }); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/2d-drawpoint.png'); - }); - - it('can draw debug line', async () => { - const canvasElement = testCanvasElement; - canvasElement.width = 100; - canvasElement.height = 100; - const sut = new ex.ExcaliburGraphicsContext2DCanvas({ - canvasElement, - enableTransparency: false, - backgroundColor: ex.Color.White - }); - - sut.clear(); - sut.debug.drawLine(ex.vec(0, 0), ex.vec(100, 100), { - color: ex.Color.Blue + describe('@visual', () => { + it('can draw a graphic', async () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContext2DCanvas({ + canvasElement, + enableTransparency: false, + backgroundColor: ex.Color.White + }); + + const rect = new ex.Rectangle({ + width: 50, + height: 50, + color: ex.Color.Blue + }); + + sut.clear(); + sut.drawImage(rect._bitmap, 20, 20); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/2d-drawgraphic.png'); }); - // chromium seems to alias this slightly differently than ff/webkit. 0.993 tolerance needed. - await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/2d-drawline.png', 0.993); - }); - - it('can transform the context', async () => { - const canvasElement = testCanvasElement; - canvasElement.width = 100; - canvasElement.height = 100; - const sut = new ex.ExcaliburGraphicsContext2DCanvas({ - canvasElement, - enableTransparency: false, - backgroundColor: ex.Color.White + it('can draw debug point', async () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContext2DCanvas({ + canvasElement, + enableTransparency: false, + backgroundColor: ex.Color.White + }); + + sut.clear(); + sut.debug.drawPoint(ex.vec(50, 50), { + size: 20, + color: ex.Color.Blue + }); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/2d-drawpoint.png'); }); - const rect = new ex.Rectangle({ - width: 50, - height: 50, - color: ex.Color.Blue + it('can draw debug line', async () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContext2DCanvas({ + canvasElement, + enableTransparency: false, + backgroundColor: ex.Color.White + }); + + sut.clear(); + sut.debug.drawLine(ex.vec(0, 0), ex.vec(100, 100), { + color: ex.Color.Blue + }); + + // chromium seems to alias this slightly differently than ff/webkit. 0.993 tolerance needed. + await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/2d-drawline.png', 0.993); }); - sut.clear(); - sut.save(); - sut.opacity = 0.5; - sut.translate(50, 50); - sut.rotate(Math.PI / 4); - sut.scale(0.5, 0.5); - sut.drawImage(rect._bitmap, -25, -25); - sut.restore(); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/2d-transform.png'); - }); - - it('can draw rectangle', async () => { - const canvasElement = testCanvasElement; - canvasElement.width = 100; - canvasElement.height = 100; - const sut = new ex.ExcaliburGraphicsContext2DCanvas({ - canvasElement: canvasElement, - enableTransparency: false, - backgroundColor: ex.Color.White + it('can transform the context', async () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContext2DCanvas({ + canvasElement, + enableTransparency: false, + backgroundColor: ex.Color.White + }); + + const rect = new ex.Rectangle({ + width: 50, + height: 50, + color: ex.Color.Blue + }); + + sut.clear(); + sut.save(); + sut.opacity = 0.5; + sut.translate(50, 50); + sut.rotate(Math.PI / 4); + sut.scale(0.5, 0.5); + sut.drawImage(rect._bitmap, -25, -25); + sut.restore(); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/2d-transform.png'); }); - sut.clear(); - sut.drawRectangle(ex.vec(10, 10), 80, 80, ex.Color.Blue); - sut.flush(); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/webgl-solid-rect.png'); - }); + it('can draw rectangle', async () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContext2DCanvas({ + canvasElement: canvasElement, + enableTransparency: false, + backgroundColor: ex.Color.White + }); + + sut.clear(); + sut.drawRectangle(ex.vec(10, 10), 80, 80, ex.Color.Blue); + sut.flush(); - it('can draw circle', async () => { - const canvasElement = testCanvasElement; - canvasElement.width = 100; - canvasElement.height = 100; - const sut = new ex.ExcaliburGraphicsContext2DCanvas({ - canvasElement: canvasElement, - enableTransparency: false, - backgroundColor: ex.Color.White + await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/webgl-solid-rect.png'); }); - sut.clear(); - sut.drawCircle(ex.vec(50, 50), 50, ex.Color.Blue); - sut.flush(); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/2d-circle.png'); - }); + it('can draw circle', async () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContext2DCanvas({ + canvasElement: canvasElement, + enableTransparency: false, + backgroundColor: ex.Color.White + }); + + sut.clear(); + sut.drawCircle(ex.vec(50, 50), 50, ex.Color.Blue); + sut.flush(); - it('can draw a line', async () => { - const canvasElement = testCanvasElement; - canvasElement.width = 100; - canvasElement.height = 100; - const sut = new ex.ExcaliburGraphicsContext2DCanvas({ - canvasElement: canvasElement, - enableTransparency: false, - backgroundColor: ex.Color.White + await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/2d-circle.png'); }); - sut.clear(); - sut.drawLine(ex.vec(0, 0), ex.vec(100, 100), ex.Color.Blue, 5); - sut.flush(); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/2d-line.png'); - }); + it('can draw a line', async () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContext2DCanvas({ + canvasElement: canvasElement, + enableTransparency: false, + backgroundColor: ex.Color.White + }); + + sut.clear(); + sut.drawLine(ex.vec(0, 0), ex.vec(100, 100), ex.Color.Blue, 5); + sut.flush(); - it('can snap drawings to pixel', async () => { - const canvasElement = testCanvasElement; - canvasElement.width = 100; - canvasElement.height = 100; - const sut = new ex.ExcaliburGraphicsContext2DCanvas({ - canvasElement: canvasElement, - enableTransparency: false, - snapToPixel: true, - backgroundColor: ex.Color.White + await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/2d-line.png'); }); - const rect = new ex.Rectangle({ - width: 50, - height: 50, - color: ex.Color.Blue + it('can snap drawings to pixel', async () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContext2DCanvas({ + canvasElement: canvasElement, + enableTransparency: false, + snapToPixel: true, + backgroundColor: ex.Color.White + }); + + const rect = new ex.Rectangle({ + width: 50, + height: 50, + color: ex.Color.Blue + }); + + sut.clear(); + sut.drawImage(rect._bitmap, 1.9, 1.9); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/2d-snap-to-pixel.png'); }); - - sut.clear(); - sut.drawImage(rect._bitmap, 1.9, 1.9); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/2d-snap-to-pixel.png'); }); it('can handle drawing a zero dimension image', () => { @@ -301,192 +303,6 @@ describe('The ExcaliburGraphicsContext', () => { context.dispose(); }); - it('will snap rectangles to the nearest pixel', async () => { - const canvas = testCanvasElement; - canvas.width = 10; - canvas.height = 10; - const context = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvas, - context: testContext, - backgroundColor: ex.Color.Black, - antialiasing: false, - multiSampleAntialiasing: false, - snapToPixel: true - }); - const rect = new ex.Rectangle({ - width: 2, - height: 2, - color: ex.Color.Red - }); - - context.clear(); - rect.draw(context, 0.5, 0.5); //1 - 0.001, 1 - 0.001); - context.flush(); - - await expect(canvas).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/pixel-snap.png'); - context.dispose(); - }); - - it('will snap rectangle graphics to the next pixel if close to the border', async () => { - const canvas = testCanvasElement; - canvas.width = 10; - canvas.height = 10; - const context = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvas, - context: testContext, - backgroundColor: ex.Color.Black, - antialiasing: false, - multiSampleAntialiasing: false, - snapToPixel: true - }); - const rect = new ex.Rectangle({ - width: 2, - height: 2, - color: ex.Color.Red - }); - - context.clear(); - rect.draw(context, 1 - 0.0001, 1 - 0.0001); - context.flush(); - - await expect(canvas).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/pixel-snap-next.png'); - context.dispose(); - }); - - it('will snap rectangles to the next pixel if close to the border', async () => { - const canvas = testCanvasElement; - canvas.width = 10; - canvas.height = 10; - const context = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvas, - context: testContext, - backgroundColor: ex.Color.Black, - antialiasing: false, - multiSampleAntialiasing: false, - snapToPixel: true - }); - - context.clear(); - context.drawRectangle(ex.vec(1 - 0.0001, 1 - 0.0001), 2, 2, ex.Color.Red); - context.flush(); - - await expect(canvas).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/pixel-snap-next.png'); - context.dispose(); - }); - - it('will snap lines to the next pixel if close to the border', async () => { - const canvas = testCanvasElement; - canvas.width = 10; - canvas.height = 10; - const context = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvas, - context: testContext, - backgroundColor: ex.Color.Black, - antialiasing: false, - multiSampleAntialiasing: false, - snapToPixel: true - }); - - context.clear(); - context.drawLine(ex.vec(1 - 0.0001, 2 - 0.0001), ex.vec(5, 2 - 0.0001), ex.Color.Red, 2); - context.flush(); - - await expect(canvas).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/pixel-snap-line-next.png'); - context.dispose(); - }); - - it('will snap circles to the next pixel if close to the border', async () => { - const canvas = testCanvasElement; - canvas.width = 10; - canvas.height = 10; - const context = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvas, - context: testContext, - backgroundColor: ex.Color.Black, - antialiasing: false, - multiSampleAntialiasing: false, - snapToPixel: true - }); - - context.clear(); - context.drawCircle(ex.vec(5 - 0.0001, 5 - 0.0001), 3, ex.Color.Red); - context.flush(); - - await expect(canvas).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/pixel-snap-circle-next.png'); - context.dispose(); - }); - - it('will snap points to the next pixel if close to the border', async () => { - const canvas = testCanvasElement; - canvas.width = 10; - canvas.height = 10; - const context = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvas, - context: testContext, - backgroundColor: ex.Color.Black, - antialiasing: false, - multiSampleAntialiasing: false, - snapToPixel: true - }); - - context.clear(); - context.debug.drawPoint(ex.vec(5 - 0.0001, 5 - 0.0001), { - color: ex.Color.Red, - size: 1 - }); - context.flush(); - - await expect(canvas).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/pixel-snap-point-next.png'); - context.dispose(); - }); - - it('can draw a graphic', async () => { - const canvasElement = testCanvasElement; - canvasElement.width = 100; - canvasElement.height = 100; - const sut = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvasElement, - context: testContext, - enableTransparency: false, - backgroundColor: ex.Color.White - }); - - const rect = new ex.Rectangle({ - width: 50, - height: 50, - color: ex.Color.Blue - }); - - sut.clear(); - sut.drawImage(rect._bitmap, 20, 20); - sut.flush(); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/2d-drawgraphic.png'); - sut.dispose(); - }); - - it('can draw debug point', async () => { - const canvasElement = testCanvasElement; - canvasElement.width = 100; - canvasElement.height = 100; - const sut = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvasElement, - context: testContext, - enableTransparency: false, - backgroundColor: ex.Color.White - }); - - sut.clear(); - sut.debug.drawPoint(ex.vec(50, 50), { - size: 20, - color: ex.Color.Blue - }); - sut.flush(); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/webgl-drawpoint.png'); - sut.dispose(); - }); - it('will log a warning if you attempt to draw outside the lifecycle', () => { const logger = ex.Logger.getInstance(); vi.spyOn(logger, 'warnOnce'); @@ -531,688 +347,876 @@ describe('The ExcaliburGraphicsContext', () => { sut.dispose(); }); - it('will preserve the painter order when switching renderer (no draw sorting)', async () => { - const canvasElement = testCanvasElement; - canvasElement.width = 100; - canvasElement.height = 100; - const sut = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvasElement, - context: testContext, - enableTransparency: false, - antialiasing: false, - multiSampleAntialiasing: false, - backgroundColor: ex.Color.White, - snapToPixel: false + describe('@visual', () => { + it('will snap rectangles to the nearest pixel', async () => { + const canvas = testCanvasElement; + canvas.width = 10; + canvas.height = 10; + const context = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvas, + context: testContext, + backgroundColor: ex.Color.Black, + antialiasing: false, + multiSampleAntialiasing: false, + snapToPixel: true + }); + const rect = new ex.Rectangle({ + width: 2, + height: 2, + color: ex.Color.Red + }); + + context.clear(); + rect.draw(context, 0.5, 0.5); //1 - 0.001, 1 - 0.001); + context.flush(); + + await expect(canvas).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/pixel-snap.png'); + context.dispose(); }); - const tex = new ex.ImageSource('/src/spec/assets/images/ExcaliburGraphicsContextSpec/sword.png'); - await tex.load(); + it('will snap rectangle graphics to the next pixel if close to the border', async () => { + const canvas = testCanvasElement; + canvas.width = 10; + canvas.height = 10; + const context = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvas, + context: testContext, + backgroundColor: ex.Color.Black, + antialiasing: false, + multiSampleAntialiasing: false, + snapToPixel: true + }); + const rect = new ex.Rectangle({ + width: 2, + height: 2, + color: ex.Color.Red + }); + + context.clear(); + rect.draw(context, 1 - 0.0001, 1 - 0.0001); + context.flush(); + + await expect(canvas).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/pixel-snap-next.png'); + context.dispose(); + }); - const circleRenderer = sut.get('ex.circle'); - vi.spyOn(circleRenderer, 'flush'); - const imageRenderer = sut.get('ex.image-v2'); - vi.spyOn(imageRenderer, 'flush'); - const rectangleRenderer = sut.get('ex.rectangle'); - vi.spyOn(rectangleRenderer, 'flush'); + it('will snap rectangles to the next pixel if close to the border', async () => { + const canvas = testCanvasElement; + canvas.width = 10; + canvas.height = 10; + const context = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvas, + context: testContext, + backgroundColor: ex.Color.Black, + antialiasing: false, + multiSampleAntialiasing: false, + snapToPixel: true + }); + + context.clear(); + context.drawRectangle(ex.vec(1 - 0.0001, 1 - 0.0001), 2, 2, ex.Color.Red); + context.flush(); + + await expect(canvas).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/pixel-snap-next.png'); + context.dispose(); + }); - sut.clear(); + it('will snap lines to the next pixel if close to the border', async () => { + const canvas = testCanvasElement; + canvas.width = 10; + canvas.height = 10; + const context = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvas, + context: testContext, + backgroundColor: ex.Color.Black, + antialiasing: false, + multiSampleAntialiasing: false, + snapToPixel: true + }); + + context.clear(); + context.drawLine(ex.vec(1 - 0.0001, 2 - 0.0001), ex.vec(5, 2 - 0.0001), ex.Color.Red, 2); + context.flush(); + + await expect(canvas).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/pixel-snap-line-next.png'); + context.dispose(); + }); - sut.useDrawSorting = false; + it('will snap circles to the next pixel if close to the border', async () => { + const canvas = testCanvasElement; + canvas.width = 10; + canvas.height = 10; + const context = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvas, + context: testContext, + backgroundColor: ex.Color.Black, + antialiasing: false, + multiSampleAntialiasing: false, + snapToPixel: true + }); + + context.clear(); + context.drawCircle(ex.vec(5 - 0.0001, 5 - 0.0001), 3, ex.Color.Red); + context.flush(); + + await expect(canvas).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/pixel-snap-circle-next.png'); + context.dispose(); + }); - sut.drawLine(ex.vec(0, 0), ex.vec(100, 100), ex.Color.Red, 2); - expect(rectangleRenderer.flush, 'rectangle line render not flushed yet').not.toHaveBeenCalled(); + it('will snap points to the next pixel if close to the border', async () => { + const canvas = testCanvasElement; + canvas.width = 10; + canvas.height = 10; + const context = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvas, + context: testContext, + backgroundColor: ex.Color.Black, + antialiasing: false, + multiSampleAntialiasing: false, + snapToPixel: true + }); + + context.clear(); + context.debug.drawPoint(ex.vec(5 - 0.0001, 5 - 0.0001), { + color: ex.Color.Red, + size: 1 + }); + context.flush(); + + await expect(canvas).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/pixel-snap-point-next.png'); + context.dispose(); + }); - sut.drawCircle(ex.Vector.Zero, 100, ex.Color.Red, ex.Color.Black, 2); - expect(circleRenderer.flush, 'circle is batched not flushed yet').not.toHaveBeenCalled(); - expect(rectangleRenderer.flush, 'rectangle line render').toHaveBeenCalled(); + it('can draw a graphic', async () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvasElement, + context: testContext, + enableTransparency: false, + backgroundColor: ex.Color.White + }); + + const rect = new ex.Rectangle({ + width: 50, + height: 50, + color: ex.Color.Blue + }); + + sut.clear(); + sut.drawImage(rect._bitmap, 20, 20); + sut.flush(); - sut.drawImage(tex.image, 0, 0, tex.width, tex.height, 20, 20); - expect(circleRenderer.flush, 'circle renderer switched, flush required').toHaveBeenCalled(); - expect(imageRenderer.flush, 'image batched not yet flushed').not.toHaveBeenCalled(); + await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/2d-drawgraphic.png'); + sut.dispose(); + }); - sut.drawRectangle(ex.Vector.Zero, 50, 50, ex.Color.Blue, ex.Color.Green, 2); - expect(imageRenderer.flush).toHaveBeenCalled(); + it('can draw debug point', async () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvasElement, + context: testContext, + enableTransparency: false, + backgroundColor: ex.Color.White + }); + + sut.clear(); + sut.debug.drawPoint(ex.vec(50, 50), { + size: 20, + color: ex.Color.Blue + }); + sut.flush(); - sut.flush(); + await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/webgl-drawpoint.png'); + sut.dispose(); + }); - // Rectangle renderer handles lines and rectangles why it's twice - expect(rectangleRenderer.flush).toHaveBeenCalledTimes(2); - expect(circleRenderer.flush).toHaveBeenCalledTimes(1); - expect(imageRenderer.flush).toHaveBeenCalledTimes(1); + it('will preserve the painter order when switching renderer (no draw sorting)', async () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvasElement, + context: testContext, + enableTransparency: false, + antialiasing: false, + multiSampleAntialiasing: false, + backgroundColor: ex.Color.White, + snapToPixel: false + }); - await expect(canvasElement).toEqualImage( - '/src/spec/assets/images/ExcaliburGraphicsContextSpec/painter-order-circle-image-rect.png', - 0.97 - ); - sut.dispose(); - }); + const tex = new ex.ImageSource('/src/spec/assets/images/ExcaliburGraphicsContextSpec/sword.png'); + await tex.load(); - it('(legacy image renderer) will preserve the painter order when switching renderer (no draw sorting)', async () => { - const canvasElement = testCanvasElement; - canvasElement.width = 100; - canvasElement.height = 100; - const sut = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvasElement, - context: testContext, - enableTransparency: false, - antialiasing: false, - multiSampleAntialiasing: false, - backgroundColor: ex.Color.White, - snapToPixel: false - }); - sut.imageRenderer = 'ex.image'; + const circleRenderer = sut.get('ex.circle'); + vi.spyOn(circleRenderer, 'flush'); + const imageRenderer = sut.get('ex.image-v2'); + vi.spyOn(imageRenderer, 'flush'); + const rectangleRenderer = sut.get('ex.rectangle'); + vi.spyOn(rectangleRenderer, 'flush'); - const tex = new ex.ImageSource('/src/spec/assets/images/ExcaliburGraphicsContextSpec/sword.png'); - await tex.load(); + sut.clear(); - const circleRenderer = sut.get('ex.circle'); - vi.spyOn(circleRenderer, 'flush'); - const imageRenderer = sut.get('ex.image'); - vi.spyOn(imageRenderer, 'flush'); - const rectangleRenderer = sut.get('ex.rectangle'); - vi.spyOn(rectangleRenderer, 'flush'); + sut.useDrawSorting = false; - sut.clear(); + sut.drawLine(ex.vec(0, 0), ex.vec(100, 100), ex.Color.Red, 2); + expect(rectangleRenderer.flush, 'rectangle line render not flushed yet').not.toHaveBeenCalled(); - sut.useDrawSorting = false; + sut.drawCircle(ex.Vector.Zero, 100, ex.Color.Red, ex.Color.Black, 2); + expect(circleRenderer.flush, 'circle is batched not flushed yet').not.toHaveBeenCalled(); + expect(rectangleRenderer.flush, 'rectangle line render').toHaveBeenCalled(); - sut.drawLine(ex.vec(0, 0), ex.vec(100, 100), ex.Color.Red, 2); - expect(rectangleRenderer.flush, 'rectangle line render not flushed yet').not.toHaveBeenCalled(); + sut.drawImage(tex.image, 0, 0, tex.width, tex.height, 20, 20); + expect(circleRenderer.flush, 'circle renderer switched, flush required').toHaveBeenCalled(); + expect(imageRenderer.flush, 'image batched not yet flushed').not.toHaveBeenCalled(); - sut.drawCircle(ex.Vector.Zero, 100, ex.Color.Red, ex.Color.Black, 2); - expect(circleRenderer.flush, 'circle is batched not flushed yet').not.toHaveBeenCalled(); - expect(rectangleRenderer.flush, 'rectangle line render').toHaveBeenCalled(); + sut.drawRectangle(ex.Vector.Zero, 50, 50, ex.Color.Blue, ex.Color.Green, 2); + expect(imageRenderer.flush).toHaveBeenCalled(); - sut.drawImage(tex.image, 0, 0, tex.width, tex.height, 20, 20); - expect(circleRenderer.flush, 'circle renderer switched, flush required').toHaveBeenCalled(); - expect(imageRenderer.flush, 'image batched not yet flushed').not.toHaveBeenCalled(); + sut.flush(); - sut.drawRectangle(ex.Vector.Zero, 50, 50, ex.Color.Blue, ex.Color.Green, 2); - expect(imageRenderer.flush).toHaveBeenCalled(); + // Rectangle renderer handles lines and rectangles why it's twice + expect(rectangleRenderer.flush).toHaveBeenCalledTimes(2); + expect(circleRenderer.flush).toHaveBeenCalledTimes(1); + expect(imageRenderer.flush).toHaveBeenCalledTimes(1); - sut.flush(); + await expect(canvasElement).toEqualImage( + '/src/spec/assets/images/ExcaliburGraphicsContextSpec/painter-order-circle-image-rect.png', + 0.97 + ); + sut.dispose(); + }); - // Rectangle renderer handles lines and rectangles why it's twice - expect(rectangleRenderer.flush).toHaveBeenCalledTimes(2); - expect(circleRenderer.flush).toHaveBeenCalledTimes(1); - expect(imageRenderer.flush).toHaveBeenCalledTimes(1); + it('(legacy image renderer) will preserve the painter order when switching renderer (no draw sorting)', async () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvasElement, + context: testContext, + enableTransparency: false, + antialiasing: false, + multiSampleAntialiasing: false, + backgroundColor: ex.Color.White, + snapToPixel: false + }); + sut.imageRenderer = 'ex.image'; + + const tex = new ex.ImageSource('/src/spec/assets/images/ExcaliburGraphicsContextSpec/sword.png'); + await tex.load(); + + const circleRenderer = sut.get('ex.circle'); + vi.spyOn(circleRenderer, 'flush'); + const imageRenderer = sut.get('ex.image'); + vi.spyOn(imageRenderer, 'flush'); + const rectangleRenderer = sut.get('ex.rectangle'); + vi.spyOn(rectangleRenderer, 'flush'); + + sut.clear(); + + sut.useDrawSorting = false; + + sut.drawLine(ex.vec(0, 0), ex.vec(100, 100), ex.Color.Red, 2); + expect(rectangleRenderer.flush, 'rectangle line render not flushed yet').not.toHaveBeenCalled(); + + sut.drawCircle(ex.Vector.Zero, 100, ex.Color.Red, ex.Color.Black, 2); + expect(circleRenderer.flush, 'circle is batched not flushed yet').not.toHaveBeenCalled(); + expect(rectangleRenderer.flush, 'rectangle line render').toHaveBeenCalled(); + + sut.drawImage(tex.image, 0, 0, tex.width, tex.height, 20, 20); + expect(circleRenderer.flush, 'circle renderer switched, flush required').toHaveBeenCalled(); + expect(imageRenderer.flush, 'image batched not yet flushed').not.toHaveBeenCalled(); + + sut.drawRectangle(ex.Vector.Zero, 50, 50, ex.Color.Blue, ex.Color.Green, 2); + expect(imageRenderer.flush).toHaveBeenCalled(); - await expect(canvasElement).toEqualImage( - '/src/spec/assets/images/ExcaliburGraphicsContextSpec/painter-order-circle-image-rect.png', - 0.97 - ); - sut.dispose(); - }); + sut.flush(); - it('will preserve the painter order when switching renderer (draw sorting)', async () => { - const canvasElement = testCanvasElement; - canvasElement.width = 100; - canvasElement.height = 100; - const sut = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvasElement, - context: testContext, - enableTransparency: false, - antialiasing: false, - multiSampleAntialiasing: false, - backgroundColor: ex.Color.White, - snapToPixel: false + // Rectangle renderer handles lines and rectangles why it's twice + expect(rectangleRenderer.flush).toHaveBeenCalledTimes(2); + expect(circleRenderer.flush).toHaveBeenCalledTimes(1); + expect(imageRenderer.flush).toHaveBeenCalledTimes(1); + + await expect(canvasElement).toEqualImage( + '/src/spec/assets/images/ExcaliburGraphicsContextSpec/painter-order-circle-image-rect.png', + 0.97 + ); + sut.dispose(); }); - const tex = new ex.ImageSource('/src/spec/assets/images/ExcaliburGraphicsContextSpec/sword.png'); - await tex.load(); + it('will preserve the painter order when switching renderer (draw sorting)', async () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvasElement, + context: testContext, + enableTransparency: false, + antialiasing: false, + multiSampleAntialiasing: false, + backgroundColor: ex.Color.White, + snapToPixel: false + }); + + const tex = new ex.ImageSource('/src/spec/assets/images/ExcaliburGraphicsContextSpec/sword.png'); + await tex.load(); + + const circleRenderer = sut.get('ex.circle'); + vi.spyOn(circleRenderer, 'flush'); + const imageRenderer = sut.get('ex.image-v2'); + vi.spyOn(imageRenderer, 'flush'); + const rectangleRenderer = sut.get('ex.rectangle'); + vi.spyOn(rectangleRenderer, 'flush'); + + sut.clear(); + sut.useDrawSorting = true; + + sut.drawLine(ex.vec(0, 0), ex.vec(100, 100), ex.Color.Red, 2); + + sut.drawCircle(ex.Vector.Zero, 100, ex.Color.Red, ex.Color.Black, 2); + + sut.drawImage(tex.image, 0, 0, tex.width, tex.height, 20, 20); + + // With draw sorting on, the rectangle at z=0 is part of the draw line, so setting the z = 1 forces the desired effect + sut.save(); + sut.z = 1; + sut.drawRectangle(ex.Vector.Zero, 50, 50, ex.Color.Blue, ex.Color.Green, 2); + sut.restore(); - const circleRenderer = sut.get('ex.circle'); - vi.spyOn(circleRenderer, 'flush'); - const imageRenderer = sut.get('ex.image-v2'); - vi.spyOn(imageRenderer, 'flush'); - const rectangleRenderer = sut.get('ex.rectangle'); - vi.spyOn(rectangleRenderer, 'flush'); + sut.flush(); - sut.clear(); - sut.useDrawSorting = true; + // Rectangle renderer handles lines and rectangles why it's twice + expect(rectangleRenderer.flush).toHaveBeenCalledTimes(2); + expect(circleRenderer.flush).toHaveBeenCalledTimes(1); + expect(imageRenderer.flush).toHaveBeenCalledTimes(1); - sut.drawLine(ex.vec(0, 0), ex.vec(100, 100), ex.Color.Red, 2); + await expect(canvasElement).toEqualImage( + '/src/spec/assets/images/ExcaliburGraphicsContextSpec/painter-order-circle-image-rect.png', + 0.97 + ); + sut.dispose(); + }); + it('(legacy image renderer) will preserve the painter order when switching renderer (draw sorting)', async () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvasElement, + context: testContext, + enableTransparency: false, + antialiasing: false, + multiSampleAntialiasing: false, + backgroundColor: ex.Color.White, + snapToPixel: false + }); + sut.imageRenderer = 'ex.image'; + + const tex = new ex.ImageSource('/src/spec/assets/images/ExcaliburGraphicsContextSpec/sword.png'); + await tex.load(); + + const circleRenderer = sut.get('ex.circle'); + vi.spyOn(circleRenderer, 'flush'); + const imageRenderer = sut.get('ex.image'); + vi.spyOn(imageRenderer, 'flush'); + const rectangleRenderer = sut.get('ex.rectangle'); + vi.spyOn(rectangleRenderer, 'flush'); + + sut.clear(); + sut.useDrawSorting = true; + + sut.drawLine(ex.vec(0, 0), ex.vec(100, 100), ex.Color.Red, 2); + + sut.drawCircle(ex.Vector.Zero, 100, ex.Color.Red, ex.Color.Black, 2); + + sut.drawImage(tex.image, 0, 0, tex.width, tex.height, 20, 20); + + // With draw sorting on, the rectangle at z=0 is part of the draw line, so setting the z = 1 forces the desired effect + sut.save(); + sut.z = 1; + sut.drawRectangle(ex.Vector.Zero, 50, 50, ex.Color.Blue, ex.Color.Green, 2); + sut.restore(); - sut.drawCircle(ex.Vector.Zero, 100, ex.Color.Red, ex.Color.Black, 2); + sut.flush(); - sut.drawImage(tex.image, 0, 0, tex.width, tex.height, 20, 20); + // Rectangle renderer handles lines and rectangles why it's twice + expect(rectangleRenderer.flush).toHaveBeenCalledTimes(2); + expect(circleRenderer.flush).toHaveBeenCalledTimes(1); + expect(imageRenderer.flush).toHaveBeenCalledTimes(1); - // With draw sorting on, the rectangle at z=0 is part of the draw line, so setting the z = 1 forces the desired effect - sut.save(); - sut.z = 1; - sut.drawRectangle(ex.Vector.Zero, 50, 50, ex.Color.Blue, ex.Color.Green, 2); - sut.restore(); + await expect(canvasElement).toEqualImage( + '/src/spec/assets/images/ExcaliburGraphicsContextSpec/painter-order-circle-image-rect.png', + 0.97 + ); + sut.dispose(); + }); - sut.flush(); + it('can draw debug line', async () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvasElement, + context: testContext, + enableTransparency: false, + antialiasing: false, + multiSampleAntialiasing: false, + backgroundColor: ex.Color.White + }); + + sut.clear(); + sut.debug.drawLine(ex.vec(0, 0), ex.vec(100, 100), { + color: ex.Color.Blue + }); + sut.flush(); - // Rectangle renderer handles lines and rectangles why it's twice - expect(rectangleRenderer.flush).toHaveBeenCalledTimes(2); - expect(circleRenderer.flush).toHaveBeenCalledTimes(1); - expect(imageRenderer.flush).toHaveBeenCalledTimes(1); + await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/webgl-drawline.png'); + sut.dispose(); + }); - await expect(canvasElement).toEqualImage( - '/src/spec/assets/images/ExcaliburGraphicsContextSpec/painter-order-circle-image-rect.png', - 0.97 - ); - sut.dispose(); - }); - it('(legacy image renderer) will preserve the painter order when switching renderer (draw sorting)', async () => { - const canvasElement = testCanvasElement; - canvasElement.width = 100; - canvasElement.height = 100; - const sut = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvasElement, - context: testContext, - enableTransparency: false, - antialiasing: false, - multiSampleAntialiasing: false, - backgroundColor: ex.Color.White, - snapToPixel: false - }); - sut.imageRenderer = 'ex.image'; - - const tex = new ex.ImageSource('/src/spec/assets/images/ExcaliburGraphicsContextSpec/sword.png'); - await tex.load(); - - const circleRenderer = sut.get('ex.circle'); - vi.spyOn(circleRenderer, 'flush'); - const imageRenderer = sut.get('ex.image'); - vi.spyOn(imageRenderer, 'flush'); - const rectangleRenderer = sut.get('ex.rectangle'); - vi.spyOn(rectangleRenderer, 'flush'); - - sut.clear(); - sut.useDrawSorting = true; - - sut.drawLine(ex.vec(0, 0), ex.vec(100, 100), ex.Color.Red, 2); - - sut.drawCircle(ex.Vector.Zero, 100, ex.Color.Red, ex.Color.Black, 2); - - sut.drawImage(tex.image, 0, 0, tex.width, tex.height, 20, 20); - - // With draw sorting on, the rectangle at z=0 is part of the draw line, so setting the z = 1 forces the desired effect - sut.save(); - sut.z = 1; - sut.drawRectangle(ex.Vector.Zero, 50, 50, ex.Color.Blue, ex.Color.Green, 2); - sut.restore(); - - sut.flush(); - - // Rectangle renderer handles lines and rectangles why it's twice - expect(rectangleRenderer.flush).toHaveBeenCalledTimes(2); - expect(circleRenderer.flush).toHaveBeenCalledTimes(1); - expect(imageRenderer.flush).toHaveBeenCalledTimes(1); - - await expect(canvasElement).toEqualImage( - '/src/spec/assets/images/ExcaliburGraphicsContextSpec/painter-order-circle-image-rect.png', - 0.97 - ); - sut.dispose(); - }); - - it('can draw debug line', async () => { - const canvasElement = testCanvasElement; - canvasElement.width = 100; - canvasElement.height = 100; - const sut = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvasElement, - context: testContext, - enableTransparency: false, - antialiasing: false, - multiSampleAntialiasing: false, - backgroundColor: ex.Color.White - }); - - sut.clear(); - sut.debug.drawLine(ex.vec(0, 0), ex.vec(100, 100), { - color: ex.Color.Blue - }); - sut.flush(); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/webgl-drawline.png'); - sut.dispose(); - }); - - it('can draw debug rectangle', async () => { - const canvasElement = testCanvasElement; - canvasElement.width = 100; - canvasElement.height = 100; - const sut = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvasElement, - context: testContext, - enableTransparency: false, - antialiasing: false, - multiSampleAntialiasing: false, - backgroundColor: ex.Color.White - }); - - sut.clear(); - sut.debug.drawRect(10, 10, 80, 80, { - color: ex.Color.Blue - }); - sut.flush(); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/webgl-rect.png'); - sut.dispose(); - }); + it('can draw debug rectangle', async () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvasElement, + context: testContext, + enableTransparency: false, + antialiasing: false, + multiSampleAntialiasing: false, + backgroundColor: ex.Color.White + }); + + sut.clear(); + sut.debug.drawRect(10, 10, 80, 80, { + color: ex.Color.Blue + }); + sut.flush(); - it('can draw rectangle', async () => { - const canvasElement = testCanvasElement; - canvasElement.width = 100; - canvasElement.height = 100; - const sut = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvasElement, - context: testContext, - enableTransparency: false, - backgroundColor: ex.Color.White + await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/webgl-rect.png'); + sut.dispose(); }); - sut.clear(); - sut.drawRectangle(ex.vec(10, 10), 80, 80, ex.Color.Blue); - sut.flush(); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/webgl-solid-rect.png'); - sut.dispose(); - }); + it('can draw rectangle', async () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvasElement, + context: testContext, + enableTransparency: false, + backgroundColor: ex.Color.White + }); + + sut.clear(); + sut.drawRectangle(ex.vec(10, 10), 80, 80, ex.Color.Blue); + sut.flush(); - it('can draw circle', async () => { - const canvasElement = testCanvasElement; - canvasElement.width = 100; - canvasElement.height = 100; - const sut = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvasElement, - context: testContext, - enableTransparency: false, - backgroundColor: ex.Color.White + await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/webgl-solid-rect.png'); + sut.dispose(); }); - sut.clear(); - sut.drawCircle(ex.vec(50, 50), 50, ex.Color.Blue); - sut.flush(); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/webgl-circle.png'); - sut.dispose(); - }); + it('can draw circle', async () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvasElement, + context: testContext, + enableTransparency: false, + backgroundColor: ex.Color.White + }); + + sut.clear(); + sut.drawCircle(ex.vec(50, 50), 50, ex.Color.Blue); + sut.flush(); - it('can draw circle with opacity', async () => { - const canvasElement = testCanvasElement; - canvasElement.width = 100; - canvasElement.height = 100; - const sut = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvasElement, - context: testContext, - enableTransparency: false, - backgroundColor: ex.Color.White + await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/webgl-circle.png'); + sut.dispose(); }); - sut.clear(); - sut.opacity = 0.05; - sut.drawCircle(ex.vec(50, 50), 50, ex.Color.Blue); - sut.flush(); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/webgl-circle-with-opacity.png'); - sut.dispose(); - }); + it('can draw circle with opacity', async () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvasElement, + context: testContext, + enableTransparency: false, + backgroundColor: ex.Color.White + }); + + sut.clear(); + sut.opacity = 0.05; + sut.drawCircle(ex.vec(50, 50), 50, ex.Color.Blue); + sut.flush(); - // FIXME these batch tests really kill test performance - // I think there might be a better way to test this with configurable batch sizes - it.skip('can draw circles in batches (no draw sorting)', () => { - const canvasElement = testCanvasElement; - canvasElement.width = 100; - canvasElement.height = 100; - const sut = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvasElement, - context: testContext, - enableTransparency: false, - backgroundColor: ex.Color.White + await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/webgl-circle-with-opacity.png'); + sut.dispose(); }); - sut.useDrawSorting = false; - sut.clear(); - const circleRenderer = sut.get('ex.circle'); - vi.spyOn(circleRenderer, 'flush'); - for (let i = 0; i < 10922; i++) { - sut.drawCircle(ex.vec(50, 50), 50, ex.Color.Blue); - } - expect(circleRenderer.flush).toHaveBeenCalledTimes(0); - - sut.drawCircle(ex.vec(50, 50), 50, ex.Color.Blue); - expect(circleRenderer.flush).toHaveBeenCalledTimes(1); + // FIXME these batch tests really kill test performance + // I think there might be a better way to test this with configurable batch sizes + it.skip('can draw circles in batches (no draw sorting)', () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvasElement, + context: testContext, + enableTransparency: false, + backgroundColor: ex.Color.White + }); + sut.useDrawSorting = false; + sut.clear(); + + const circleRenderer = sut.get('ex.circle'); + vi.spyOn(circleRenderer, 'flush'); + for (let i = 0; i < 10922; i++) { + sut.drawCircle(ex.vec(50, 50), 50, ex.Color.Blue); + } + expect(circleRenderer.flush).toHaveBeenCalledTimes(0); - for (let i = 0; i < 10922; i++) { sut.drawCircle(ex.vec(50, 50), 50, ex.Color.Blue); - } - expect(circleRenderer.flush).toHaveBeenCalledTimes(2); - sut.dispose(); - }); + expect(circleRenderer.flush).toHaveBeenCalledTimes(1); - it.skip('can draw circles in batches (draw sorting)', () => { - const canvasElement = testCanvasElement; - canvasElement.width = 100; - canvasElement.height = 100; - const sut = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvasElement, - context: testContext, - enableTransparency: false, - backgroundColor: ex.Color.White + for (let i = 0; i < 10922; i++) { + sut.drawCircle(ex.vec(50, 50), 50, ex.Color.Blue); + } + expect(circleRenderer.flush).toHaveBeenCalledTimes(2); + sut.dispose(); }); - sut.useDrawSorting = true; - sut.clear(); - - const circleRenderer = sut.get('ex.circle'); - vi.spyOn(circleRenderer, 'flush'); - for (let i = 0; i < 10922; i++) { - sut.drawCircle(ex.vec(50, 50), 50, ex.Color.Blue); - } - sut.drawCircle(ex.vec(50, 50), 50, ex.Color.Blue); + it.skip('can draw circles in batches (draw sorting)', () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvasElement, + context: testContext, + enableTransparency: false, + backgroundColor: ex.Color.White + }); + sut.useDrawSorting = true; + sut.clear(); + + const circleRenderer = sut.get('ex.circle'); + vi.spyOn(circleRenderer, 'flush'); + for (let i = 0; i < 10922; i++) { + sut.drawCircle(ex.vec(50, 50), 50, ex.Color.Blue); + } - for (let i = 0; i < 10922; i++) { sut.drawCircle(ex.vec(50, 50), 50, ex.Color.Blue); - } - sut.flush(); - expect(circleRenderer.flush).toHaveBeenCalledTimes(3); - sut.dispose(); - }); - it.skip('can draw rectangles in batches (no draw sorting)', () => { - const canvasElement = testCanvasElement; - canvasElement.width = 100; - canvasElement.height = 100; - const sut = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvasElement, - context: testContext, - enableTransparency: false, - backgroundColor: ex.Color.White + for (let i = 0; i < 10922; i++) { + sut.drawCircle(ex.vec(50, 50), 50, ex.Color.Blue); + } + sut.flush(); + expect(circleRenderer.flush).toHaveBeenCalledTimes(3); + sut.dispose(); }); - sut.useDrawSorting = false; - - sut.clear(); - - const rectangleRenderer = sut.get('ex.rectangle'); - vi.spyOn(rectangleRenderer, 'flush'); - for (let i = 0; i < 10922; i++) { - sut.drawRectangle(ex.vec(50, 50), 50, 50, ex.Color.Blue); - } - expect(rectangleRenderer.flush).toHaveBeenCalledTimes(0); - - sut.drawRectangle(ex.vec(50, 50), 50, 50, ex.Color.Blue); - expect(rectangleRenderer.flush).toHaveBeenCalledTimes(1); + it.skip('can draw rectangles in batches (no draw sorting)', () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvasElement, + context: testContext, + enableTransparency: false, + backgroundColor: ex.Color.White + }); + + sut.useDrawSorting = false; + + sut.clear(); + + const rectangleRenderer = sut.get('ex.rectangle'); + vi.spyOn(rectangleRenderer, 'flush'); + for (let i = 0; i < 10922; i++) { + sut.drawRectangle(ex.vec(50, 50), 50, 50, ex.Color.Blue); + } + expect(rectangleRenderer.flush).toHaveBeenCalledTimes(0); - for (let i = 0; i < 10922; i++) { sut.drawRectangle(ex.vec(50, 50), 50, 50, ex.Color.Blue); - } - expect(rectangleRenderer.flush).toHaveBeenCalledTimes(2); - sut.dispose(); - }); + expect(rectangleRenderer.flush).toHaveBeenCalledTimes(1); - it.skip('can draw rectangles in batches (draw sorting)', () => { - const canvasElement = testCanvasElement; - canvasElement.width = 100; - canvasElement.height = 100; - const sut = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvasElement, - context: testContext, - enableTransparency: false, - backgroundColor: ex.Color.White + for (let i = 0; i < 10922; i++) { + sut.drawRectangle(ex.vec(50, 50), 50, 50, ex.Color.Blue); + } + expect(rectangleRenderer.flush).toHaveBeenCalledTimes(2); + sut.dispose(); }); - sut.useDrawSorting = true; - - sut.clear(); - - const rectangleRenderer = sut.get('ex.rectangle'); - vi.spyOn(rectangleRenderer, 'flush'); - for (let i = 0; i < 10922; i++) { - sut.drawRectangle(ex.vec(50, 50), 50, 50, ex.Color.Blue); - } - - sut.drawRectangle(ex.vec(50, 50), 50, 50, ex.Color.Blue); + it.skip('can draw rectangles in batches (draw sorting)', () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvasElement, + context: testContext, + enableTransparency: false, + backgroundColor: ex.Color.White + }); + + sut.useDrawSorting = true; + + sut.clear(); + + const rectangleRenderer = sut.get('ex.rectangle'); + vi.spyOn(rectangleRenderer, 'flush'); + for (let i = 0; i < 10922; i++) { + sut.drawRectangle(ex.vec(50, 50), 50, 50, ex.Color.Blue); + } - for (let i = 0; i < 10922; i++) { sut.drawRectangle(ex.vec(50, 50), 50, 50, ex.Color.Blue); - } - sut.flush(); - expect(rectangleRenderer.flush).toHaveBeenCalledTimes(3); - sut.dispose(); - }); + for (let i = 0; i < 10922; i++) { + sut.drawRectangle(ex.vec(50, 50), 50, 50, ex.Color.Blue); + } - it.skip('can draw images in batches (no draw sorting)', async () => { - const canvasElement = testCanvasElement; - canvasElement.width = 100; - canvasElement.height = 100; - const sut = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvasElement, - context: testContext, - enableTransparency: false, - backgroundColor: ex.Color.White + sut.flush(); + expect(rectangleRenderer.flush).toHaveBeenCalledTimes(3); + sut.dispose(); }); - sut.useDrawSorting = false; - - sut.clear(); - - const tex = new ex.ImageSource('/src/spec/assets/images/ExcaliburGraphicsContextSpec/sword.png'); - await tex.load(); - - const imageRenderer = sut.get('ex.image'); - vi.spyOn(imageRenderer, 'flush'); - for (let i = 0; i < 10922; i++) { - sut.drawImage(tex.image, 0, 0); - } - expect(imageRenderer.flush).toHaveBeenCalledTimes(0); - sut.drawImage(tex.image, 0, 0); - expect(imageRenderer.flush).toHaveBeenCalledTimes(1); + it.skip('can draw images in batches (no draw sorting)', async () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvasElement, + context: testContext, + enableTransparency: false, + backgroundColor: ex.Color.White + }); + sut.useDrawSorting = false; + + sut.clear(); + + const tex = new ex.ImageSource('/src/spec/assets/images/ExcaliburGraphicsContextSpec/sword.png'); + await tex.load(); + + const imageRenderer = sut.get('ex.image'); + vi.spyOn(imageRenderer, 'flush'); + for (let i = 0; i < 10922; i++) { + sut.drawImage(tex.image, 0, 0); + } + expect(imageRenderer.flush).toHaveBeenCalledTimes(0); - for (let i = 0; i < 10922; i++) { sut.drawImage(tex.image, 0, 0); - } - expect(imageRenderer.flush).toHaveBeenCalledTimes(2); - sut.dispose(); - }); + expect(imageRenderer.flush).toHaveBeenCalledTimes(1); - it.skip('can draw images in batches (draw sorting)', async () => { - const canvasElement = testCanvasElement; - canvasElement.width = 100; - canvasElement.height = 100; - const sut = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvasElement, - context: testContext, - enableTransparency: false, - backgroundColor: ex.Color.White + for (let i = 0; i < 10922; i++) { + sut.drawImage(tex.image, 0, 0); + } + expect(imageRenderer.flush).toHaveBeenCalledTimes(2); + sut.dispose(); }); - sut.useDrawSorting = true; - - sut.clear(); - - const tex = new ex.ImageSource('/src/spec/assets/images/ExcaliburGraphicsContextSpec/sword.png'); - await tex.load(); - - const imageRenderer = sut.get('ex.image'); - vi.spyOn(imageRenderer, 'flush'); - for (let i = 0; i < 10922; i++) { - sut.drawImage(tex.image, 0, 0); - } - sut.drawImage(tex.image, 0, 0); + it.skip('can draw images in batches (draw sorting)', async () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvasElement, + context: testContext, + enableTransparency: false, + backgroundColor: ex.Color.White + }); + sut.useDrawSorting = true; + + sut.clear(); + + const tex = new ex.ImageSource('/src/spec/assets/images/ExcaliburGraphicsContextSpec/sword.png'); + await tex.load(); + + const imageRenderer = sut.get('ex.image'); + vi.spyOn(imageRenderer, 'flush'); + for (let i = 0; i < 10922; i++) { + sut.drawImage(tex.image, 0, 0); + } - for (let i = 0; i < 10922; i++) { sut.drawImage(tex.image, 0, 0); - } - sut.flush(); - expect(imageRenderer.flush).toHaveBeenCalledTimes(3); - sut.dispose(); - }); - - it('can draw a line', async () => { - const canvasElement = testCanvasElement; - canvasElement.width = 100; - canvasElement.height = 100; - const sut = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvasElement, - context: testContext, - enableTransparency: false, - antialiasing: false, - multiSampleAntialiasing: false, - backgroundColor: ex.Color.White, - snapToPixel: false - }); - - sut.clear(); - sut.drawLine(ex.vec(0, 0), ex.vec(100, 100), ex.Color.Blue, 5); - sut.flush(); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/webgl-line.png'); - sut.dispose(); - }); - - it('can transform the context', async () => { - const canvasElement = testCanvasElement; - canvasElement.width = 100; - canvasElement.height = 100; - const sut = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvasElement, - context: testContext, - enableTransparency: false, - antialiasing: false, - multiSampleAntialiasing: false, - backgroundColor: ex.Color.White - }); - - const rect = new ex.Rectangle({ - width: 50, - height: 50, - color: ex.Color.Blue - }); - - sut.clear(); - sut.save(); - sut.opacity = 0.5; - sut.translate(50, 50); - sut.rotate(Math.PI / 4); - sut.scale(0.5, 0.5); - sut.drawImage(rect._bitmap, -25, -25); - sut.restore(); - sut.flush(); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/webgl-transform.png'); - sut.dispose(); - }); - it('can snap drawings to pixel', async () => { - const canvasElement = testCanvasElement; - canvasElement.width = 100; - canvasElement.height = 100; - const sut = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvasElement, - context: testContext, - enableTransparency: false, - snapToPixel: true, - backgroundColor: ex.Color.White + for (let i = 0; i < 10922; i++) { + sut.drawImage(tex.image, 0, 0); + } + sut.flush(); + expect(imageRenderer.flush).toHaveBeenCalledTimes(3); + sut.dispose(); }); - const rect = new ex.Rectangle({ - width: 50, - height: 50, - color: ex.Color.Blue + it('can draw a line', async () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvasElement, + context: testContext, + enableTransparency: false, + antialiasing: false, + multiSampleAntialiasing: false, + backgroundColor: ex.Color.White, + snapToPixel: false + }); + + sut.clear(); + sut.drawLine(ex.vec(0, 0), ex.vec(100, 100), ex.Color.Blue, 5); + sut.flush(); + await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/webgl-line.png'); + sut.dispose(); }); - sut.clear(); - sut.drawImage(rect._bitmap, 1.9, 1.9); - sut.flush(); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/2d-snap-to-pixel.png'); - sut.dispose(); - }); + it('can transform the context', async () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvasElement, + context: testContext, + enableTransparency: false, + antialiasing: false, + multiSampleAntialiasing: false, + backgroundColor: ex.Color.White + }); + + const rect = new ex.Rectangle({ + width: 50, + height: 50, + color: ex.Color.Blue + }); + + sut.clear(); + sut.save(); + sut.opacity = 0.5; + sut.translate(50, 50); + sut.rotate(Math.PI / 4); + sut.scale(0.5, 0.5); + sut.drawImage(rect._bitmap, -25, -25); + sut.restore(); + sut.flush(); - it('can handle drawing a zero dimension image', () => { - const canvasElement = testCanvasElement; - canvasElement.width = 100; - canvasElement.height = 100; - const sut = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvasElement, - context: testContext, - enableTransparency: false, - snapToPixel: true, - backgroundColor: ex.Color.White + await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/webgl-transform.png'); + sut.dispose(); }); - const rect = new ex.Rectangle({ - width: 0, - height: 0, - color: ex.Color.Blue - }); - sut.clear(); - - expect(() => { - sut.drawImage(rect._bitmap, 0, 0); - }).not.toThrow(); - expect(() => { - sut.drawImage(rect._bitmap, 0, 0, 0, 0); + it('can snap drawings to pixel', async () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvasElement, + context: testContext, + enableTransparency: false, + snapToPixel: true, + backgroundColor: ex.Color.White + }); + + const rect = new ex.Rectangle({ + width: 50, + height: 50, + color: ex.Color.Blue + }); + + sut.clear(); + sut.drawImage(rect._bitmap, 1.9, 1.9); sut.flush(); - }).not.toThrow(); - - expect(() => { - sut.drawImage(rect._bitmap, 0, 0, 10, 10, 0, 0, 0, 0); - sut.flush(); - }).not.toThrow(); - sut.dispose(); - }); - it('can handle rendering rasters in succession', async () => { - // arrange - const image = new ex.ImageSource('/src/spec/assets/images/ExcaliburGraphicsContextSpec/test.png'); - await image.load(); - const spriteSheet = ex.SpriteSheet.fromImageSource({ - image: image, - grid: { - rows: 1, - columns: 4, - spriteWidth: 100, - spriteHeight: 100 - } + await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/2d-snap-to-pixel.png'); + sut.dispose(); }); - const down = ex.Animation.fromSpriteSheet(spriteSheet, [0, 1, 2, 3], 500); - const polygon = new ex.Polygon({ - points: [ex.vec(50, 0), ex.vec(150, 0), ex.vec(200, 86), ex.vec(150, 172), ex.vec(50, 172), ex.vec(0, 86)], - color: ex.Color.fromRGB(0, 255, 0) - }); - const rect = new ex.Rectangle({ width: 100, height: 100, color: ex.Color.fromRGB(255, 0, 0) }); - const circle = new ex.Circle({ radius: 50, color: ex.Color.fromRGB(0, 0, 255) }); - - const canvasElement = testCanvasElement; - canvasElement.width = 500; - canvasElement.height = 400; - const sut = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvasElement, - context: testContext, - enableTransparency: false, - snapToPixel: true, - backgroundColor: ex.Color.White + it('can handle drawing a zero dimension image', () => { + const canvasElement = testCanvasElement; + canvasElement.width = 100; + canvasElement.height = 100; + const sut = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvasElement, + context: testContext, + enableTransparency: false, + snapToPixel: true, + backgroundColor: ex.Color.White + }); + const rect = new ex.Rectangle({ + width: 0, + height: 0, + color: ex.Color.Blue + }); + sut.clear(); + + expect(() => { + sut.drawImage(rect._bitmap, 0, 0); + }).not.toThrow(); + + expect(() => { + sut.drawImage(rect._bitmap, 0, 0, 0, 0); + sut.flush(); + }).not.toThrow(); + + expect(() => { + sut.drawImage(rect._bitmap, 0, 0, 10, 10, 0, 0, 0, 0); + sut.flush(); + }).not.toThrow(); + sut.dispose(); }); - // act - - sut.clear(); - - down.draw(sut, 100, 100); - polygon.draw(sut, 350, 100); - rect.draw(sut, 300, 300); - circle.draw(sut, 400, 300); + it('can handle rendering rasters in succession', async () => { + // arrange + const image = new ex.ImageSource('/src/spec/assets/images/ExcaliburGraphicsContextSpec/test.png'); + await image.load(); + const spriteSheet = ex.SpriteSheet.fromImageSource({ + image: image, + grid: { + rows: 1, + columns: 4, + spriteWidth: 100, + spriteHeight: 100 + } + }); + + const down = ex.Animation.fromSpriteSheet(spriteSheet, [0, 1, 2, 3], 500); + const polygon = new ex.Polygon({ + points: [ex.vec(50, 0), ex.vec(150, 0), ex.vec(200, 86), ex.vec(150, 172), ex.vec(50, 172), ex.vec(0, 86)], + color: ex.Color.fromRGB(0, 255, 0) + }); + const rect = new ex.Rectangle({ width: 100, height: 100, color: ex.Color.fromRGB(255, 0, 0) }); + const circle = new ex.Circle({ radius: 50, color: ex.Color.fromRGB(0, 0, 255) }); + + const canvasElement = testCanvasElement; + canvasElement.width = 500; + canvasElement.height = 400; + const sut = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvasElement, + context: testContext, + enableTransparency: false, + snapToPixel: true, + backgroundColor: ex.Color.White + }); + + // act + + sut.clear(); + + down.draw(sut, 100, 100); + polygon.draw(sut, 350, 100); + rect.draw(sut, 300, 300); + circle.draw(sut, 400, 300); + + down.tick(500, 1); + down.draw(sut, 100, 100); + polygon.draw(sut, 350, 100); + rect.draw(sut, 300, 300); + circle.draw(sut, 400, 300); - down.tick(500, 1); - down.draw(sut, 100, 100); - polygon.draw(sut, 350, 100); - rect.draw(sut, 300, 300); - circle.draw(sut, 400, 300); - - sut.flush(); + sut.flush(); - // assert - await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/rasters.png'); - sut.dispose(); + // assert + await expect(canvasElement).toEqualImage('/src/spec/assets/images/ExcaliburGraphicsContextSpec/rasters.png'); + sut.dispose(); + }); }); }); }); diff --git a/src/spec/vitest/FadeInOutSpec.ts b/src/spec/vitest/FadeInOutSpec.ts index 7e861f50..49f1534e 100644 --- a/src/spec/vitest/FadeInOutSpec.ts +++ b/src/spec/vitest/FadeInOutSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('A FadeInOut transition', () => { it('exists', () => { @@ -13,7 +13,7 @@ describe('A FadeInOut transition', () => { expect(sut.color).toEqual(ex.Color.Red); }); - it('can fade in', async () => { + it('@visual can fade in', async () => { const engine = TestUtils.engine({ backgroundColor: ex.Color.ExcaliburBlue }); const clock = engine.clock as ex.TestClock; await TestUtils.runToReady(engine); @@ -51,7 +51,7 @@ describe('A FadeInOut transition', () => { engine.dispose(); }); - it('can fade out', async () => { + it('@visual can fade out', async () => { const engine = TestUtils.engine({ backgroundColor: ex.Color.ExcaliburBlue }); const clock = engine.clock as ex.TestClock; await TestUtils.runToReady(engine); diff --git a/src/spec/vitest/FrameStatsSpec.ts b/src/spec/vitest/FrameStatsSpec.ts index 1de0a66c..06f6380d 100644 --- a/src/spec/vitest/FrameStatsSpec.ts +++ b/src/spec/vitest/FrameStatsSpec.ts @@ -1,6 +1,6 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; -import { Mocks } from './util/Mocks'; +import { TestUtils } from '../__util__/TestUtils'; +import { Mocks } from '../__util__/Mocks'; import type { TestClock } from '@excalibur'; describe('The engine', () => { diff --git a/src/spec/vitest/GamepadSpec.ts b/src/spec/vitest/GamepadSpec.ts index 87d46d30..38f0b1e7 100644 --- a/src/spec/vitest/GamepadSpec.ts +++ b/src/spec/vitest/GamepadSpec.ts @@ -1,6 +1,6 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; -import { Mocks } from './util/Mocks'; +import { TestUtils } from '../__util__/TestUtils'; +import { Mocks } from '../__util__/Mocks'; describe('A gamepad', () => { let engine: ex.Engine; diff --git a/src/spec/vitest/GifSpec.ts b/src/spec/vitest/GifSpec.ts index 0ee9fb65..3a1648b3 100644 --- a/src/spec/vitest/GifSpec.ts +++ b/src/spec/vitest/GifSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('A Gif', () => { let engine: ex.Engine; @@ -27,92 +27,94 @@ describe('A Gif', () => { }); })); - it('should parse gifs that have lct & anim params', async () => { - const sut = new ex.Gif('/src/spec/assets/images/GifSpec/loading-screen.gif'); - await sut.load(); + describe('@visual', () => { + it('should parse gifs that have lct & anim params', async () => { + const sut = new ex.Gif('/src/spec/assets/images/GifSpec/loading-screen.gif'); + await sut.load(); - const sprite = sut.toSprite(20); - expect(sut.isLoaded()).toBe(true); + const sprite = sut.toSprite(20); + expect(sut.isLoaded()).toBe(true); - sprite.draw(engine.graphicsContext, 0, 0); - engine.graphicsContext.flush(); + sprite.draw(engine.graphicsContext, 0, 0); + engine.graphicsContext.flush(); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GifSpec/loading-frame-20.png'); - }); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GifSpec/loading-frame-20.png'); + }); - it('should parse gifs that have animations with sub frames', async () => { - const sut = new ex.Gif('/src/spec/assets/images/GifSpec/stoplight.gif'); - await sut.load(); - expect(sut.isLoaded()).toBe(true); + it('should parse gifs that have animations with sub frames', async () => { + const sut = new ex.Gif('/src/spec/assets/images/GifSpec/stoplight.gif'); + await sut.load(); + expect(sut.isLoaded()).toBe(true); - const sprite2 = sut.toSprite(2); - sprite2.draw(engine.graphicsContext, 0, 0); - engine.graphicsContext.flush(); + const sprite2 = sut.toSprite(2); + sprite2.draw(engine.graphicsContext, 0, 0); + engine.graphicsContext.flush(); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GifSpec/stoplight-frame-3.png'); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GifSpec/stoplight-frame-3.png'); - const sprite1 = sut.toSprite(1); - sprite1.draw(engine.graphicsContext, 0, 0); - engine.graphicsContext.flush(); + const sprite1 = sut.toSprite(1); + sprite1.draw(engine.graphicsContext, 0, 0); + engine.graphicsContext.flush(); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GifSpec/stoplight-frame-2.png'); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GifSpec/stoplight-frame-2.png'); - const sprite0 = sut.toSprite(0); - sprite0.draw(engine.graphicsContext, 0, 0); - engine.graphicsContext.flush(); + const sprite0 = sut.toSprite(0); + sprite0.draw(engine.graphicsContext, 0, 0); + engine.graphicsContext.flush(); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GifSpec/stoplight-frame-1.png'); - }); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GifSpec/stoplight-frame-1.png'); + }); - it('should load each frame', async () => { - await gif.load(); - expect(gif).toBeDefined(); + it('should load each frame', async () => { + await gif.load(); + expect(gif).toBeDefined(); - const spriteFrame: ex.Sprite = gif.toSprite(); - expect(spriteFrame).toBeDefined(); - expect(spriteFrame.height).toBe(100); - expect(spriteFrame.width).toBe(100); + const spriteFrame: ex.Sprite = gif.toSprite(); + expect(spriteFrame).toBeDefined(); + expect(spriteFrame.height).toBe(100); + expect(spriteFrame.width).toBe(100); - let sprite: ex.Sprite = gif.toSprite(); - expect(gif.isLoaded()).toBe(true); - sprite.draw(engine.graphicsContext, 0, 0); - engine.graphicsContext.flush(); + let sprite: ex.Sprite = gif.toSprite(); + expect(gif.isLoaded()).toBe(true); + sprite.draw(engine.graphicsContext, 0, 0); + engine.graphicsContext.flush(); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GifSpec/frame1.png'); - engine.graphicsContext.backgroundColor = ex.Color.Transparent; - engine.graphicsContext.clear(); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GifSpec/frame1.png'); + engine.graphicsContext.backgroundColor = ex.Color.Transparent; + engine.graphicsContext.clear(); - sprite = gif.toSprite(1); - expect(gif.isLoaded()).toBe(true); - sprite.draw(engine.graphicsContext, 0, 0); - engine.graphicsContext.flush(); + sprite = gif.toSprite(1); + expect(gif.isLoaded()).toBe(true); + sprite.draw(engine.graphicsContext, 0, 0); + engine.graphicsContext.flush(); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GifSpec/frame2.png'); - }); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GifSpec/frame2.png'); + }); - it('should be read as a SpriteSheet', async () => { - await gif.load(); - expect(gif).toBeDefined(); - const spriteSheet: ex.SpriteSheet = gif.toSpriteSheet(); - const sprite = spriteSheet.getSprite(0, 0); - sprite.draw(engine.graphicsContext, 0, 0); - engine.graphicsContext.flush(); + it('should be read as a SpriteSheet', async () => { + await gif.load(); + expect(gif).toBeDefined(); + const spriteSheet: ex.SpriteSheet = gif.toSpriteSheet(); + const sprite = spriteSheet.getSprite(0, 0); + sprite.draw(engine.graphicsContext, 0, 0); + engine.graphicsContext.flush(); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GifSpec/frame1.png'); - }); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GifSpec/frame1.png'); + }); - it('should be read as an Animation', async () => { - await gif.load(); - expect(gif).toBeDefined(); - const animation: ex.Animation = gif.toAnimation(500); + it('should be read as an Animation', async () => { + await gif.load(); + expect(gif).toBeDefined(); + const animation: ex.Animation = gif.toAnimation(500); - expect(animation.frames.length).toBe(2); - const frame1 = animation.frames[0]; - const frame2 = animation.frames[1]; + expect(animation.frames.length).toBe(2); + const frame1 = animation.frames[0]; + const frame2 = animation.frames[1]; - frame2.graphic.draw(engine.graphicsContext, 0, 0); - engine.graphicsContext.flush(); + frame2.graphic.draw(engine.graphicsContext, 0, 0); + engine.graphicsContext.flush(); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GifSpec/frame2.png'); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GifSpec/frame2.png'); + }); }); }); diff --git a/src/spec/vitest/GpuParticleSpec.ts b/src/spec/vitest/GpuParticleSpec.ts index 57c640ca..4df81e40 100644 --- a/src/spec/vitest/GpuParticleSpec.ts +++ b/src/spec/vitest/GpuParticleSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('A GPU particle', () => { let engine: ex.Engine; @@ -86,196 +86,198 @@ describe('A GPU particle', () => { expect(emitter.random.seed).toBe(1337); }); - it('should emit particles', async () => { - const emitter = new ex.GpuParticleEmitter({ - pos: new ex.Vector(400, 100), - width: 20, - height: 30, - isEmitting: true, - emitRate: 5, - particle: { - minSpeed: 100, - maxSpeed: 200, - acc: ex.Vector.Zero.clone(), - minAngle: 0, - maxAngle: Math.PI / 2, - life: 4000, - opacity: 0.5, - fade: false, - startSize: 30, - endSize: 40, - beginColor: ex.Color.Red.clone(), - endColor: ex.Color.Blue.clone(), - graphic: null, - angularVelocity: 3, - randomRotation: false - }, - focus: null, - focusAccel: null, - emitterType: ex.EmitterType.Circle, - radius: 20, - random: new ex.Random(1337) - }); - engine.backgroundColor = ex.Color.Transparent; - engine.add(emitter); - emitter.emitParticles(10); - - engine.currentScene.update(engine, 100); - engine.currentScene.update(engine, 100); - engine.currentScene.update(engine, 100); - engine.currentScene.draw(engine.graphicsContext, 100); - engine.graphicsContext.flush(); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GpuParticlesSpec/particles.png'); - }); + describe('@visual', () => { + it('should emit particles', async () => { + const emitter = new ex.GpuParticleEmitter({ + pos: new ex.Vector(400, 100), + width: 20, + height: 30, + isEmitting: true, + emitRate: 5, + particle: { + minSpeed: 100, + maxSpeed: 200, + acc: ex.Vector.Zero.clone(), + minAngle: 0, + maxAngle: Math.PI / 2, + life: 4000, + opacity: 0.5, + fade: false, + startSize: 30, + endSize: 40, + beginColor: ex.Color.Red.clone(), + endColor: ex.Color.Blue.clone(), + graphic: null, + angularVelocity: 3, + randomRotation: false + }, + focus: null, + focusAccel: null, + emitterType: ex.EmitterType.Circle, + radius: 20, + random: new ex.Random(1337) + }); + engine.backgroundColor = ex.Color.Transparent; + engine.add(emitter); + emitter.emitParticles(10); - it('should clear particles', async () => { - const emitter = new ex.GpuParticleEmitter({ - pos: new ex.Vector(400, 100), - width: 20, - height: 30, - isEmitting: true, - emitRate: 5, - particle: { - minSpeed: 100, - maxSpeed: 200, - acc: ex.Vector.Zero.clone(), - minAngle: 0, - maxAngle: Math.PI / 2, - life: 4000, - opacity: 0.5, - fade: false, - startSize: 30, - endSize: 40, - beginColor: ex.Color.Red.clone(), - endColor: ex.Color.Blue.clone(), - graphic: null, - angularVelocity: 3, - randomRotation: false - }, - focus: null, - focusAccel: null, - emitterType: ex.EmitterType.Circle, - radius: 20, - random: new ex.Random(1337) + engine.currentScene.update(engine, 100); + engine.currentScene.update(engine, 100); + engine.currentScene.update(engine, 100); + engine.currentScene.draw(engine.graphicsContext, 100); + engine.graphicsContext.flush(); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GpuParticlesSpec/particles.png'); }); - engine.backgroundColor = ex.Color.Transparent; - engine.add(emitter); - emitter.emitParticles(10); - emitter.clearParticles(); - engine.currentScene.update(engine, 100); - engine.currentScene.update(engine, 100); - engine.currentScene.update(engine, 100); - engine.currentScene.draw(engine.graphicsContext, 100); - engine.graphicsContext.flush(); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GpuParticlesSpec/clear.png'); - }); + it('should clear particles', async () => { + const emitter = new ex.GpuParticleEmitter({ + pos: new ex.Vector(400, 100), + width: 20, + height: 30, + isEmitting: true, + emitRate: 5, + particle: { + minSpeed: 100, + maxSpeed: 200, + acc: ex.Vector.Zero.clone(), + minAngle: 0, + maxAngle: Math.PI / 2, + life: 4000, + opacity: 0.5, + fade: false, + startSize: 30, + endSize: 40, + beginColor: ex.Color.Red.clone(), + endColor: ex.Color.Blue.clone(), + graphic: null, + angularVelocity: 3, + randomRotation: false + }, + focus: null, + focusAccel: null, + emitterType: ex.EmitterType.Circle, + radius: 20, + random: new ex.Random(1337) + }); + engine.backgroundColor = ex.Color.Transparent; + engine.add(emitter); + emitter.emitParticles(10); + emitter.clearParticles(); - it("should emit particles and wrap it's ring buffer", async () => { - const emitter = new ex.GpuParticleEmitter({ - pos: new ex.Vector(400, 100), - width: 20, - height: 30, - isEmitting: true, - emitRate: 5, - maxParticles: 200, - particle: { - minSpeed: 100, - maxSpeed: 200, - acc: ex.Vector.Zero.clone(), - minAngle: 0, - maxAngle: Math.PI / 2, - life: 4000, - opacity: 0.5, - fade: false, - startSize: 30, - endSize: 40, - beginColor: ex.Color.Red.clone(), - endColor: ex.Color.Blue.clone(), - graphic: null, - angularVelocity: 3, - randomRotation: false - }, - focus: null, - focusAccel: null, - emitterType: ex.EmitterType.Circle, - radius: 20, - random: new ex.Random(1337) + engine.currentScene.update(engine, 100); + engine.currentScene.update(engine, 100); + engine.currentScene.update(engine, 100); + engine.currentScene.draw(engine.graphicsContext, 100); + engine.graphicsContext.flush(); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GpuParticlesSpec/clear.png'); }); - engine.backgroundColor = ex.Color.Transparent; - engine.add(emitter); - engine.currentScene.update(engine, 100); - emitter.emitParticles(50); - engine.currentScene.draw(engine.graphicsContext, 100); - engine.graphicsContext.flush(); + it("should emit particles and wrap it's ring buffer", async () => { + const emitter = new ex.GpuParticleEmitter({ + pos: new ex.Vector(400, 100), + width: 20, + height: 30, + isEmitting: true, + emitRate: 5, + maxParticles: 200, + particle: { + minSpeed: 100, + maxSpeed: 200, + acc: ex.Vector.Zero.clone(), + minAngle: 0, + maxAngle: Math.PI / 2, + life: 4000, + opacity: 0.5, + fade: false, + startSize: 30, + endSize: 40, + beginColor: ex.Color.Red.clone(), + endColor: ex.Color.Blue.clone(), + graphic: null, + angularVelocity: 3, + randomRotation: false + }, + focus: null, + focusAccel: null, + emitterType: ex.EmitterType.Circle, + radius: 20, + random: new ex.Random(1337) + }); + engine.backgroundColor = ex.Color.Transparent; + engine.add(emitter); - engine.currentScene.update(engine, 100); - emitter.emitParticles(50); - engine.currentScene.draw(engine.graphicsContext, 100); - engine.graphicsContext.flush(); + engine.currentScene.update(engine, 100); + emitter.emitParticles(50); + engine.currentScene.draw(engine.graphicsContext, 100); + engine.graphicsContext.flush(); - engine.currentScene.update(engine, 100); - emitter.emitParticles(50); - engine.currentScene.draw(engine.graphicsContext, 100); - engine.graphicsContext.flush(); + engine.currentScene.update(engine, 100); + emitter.emitParticles(50); + engine.currentScene.draw(engine.graphicsContext, 100); + engine.graphicsContext.flush(); - engine.currentScene.update(engine, 100); - emitter.emitParticles(50); - engine.currentScene.draw(engine.graphicsContext, 100); - engine.graphicsContext.flush(); + engine.currentScene.update(engine, 100); + emitter.emitParticles(50); + engine.currentScene.draw(engine.graphicsContext, 100); + engine.graphicsContext.flush(); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GpuParticlesSpec/particles-wrapped.png'); - }); + engine.currentScene.update(engine, 100); + emitter.emitParticles(50); + engine.currentScene.draw(engine.graphicsContext, 100); + engine.graphicsContext.flush(); - it('can be parented', async () => { - const emitter = new ex.ParticleEmitter({ - pos: new ex.Vector(0, 0), - width: 20, - height: 30, - isEmitting: true, - emitRate: 5, - particle: { - minSpeed: 100, - maxSpeed: 200, - acc: ex.Vector.Zero.clone(), - minAngle: 0, - maxAngle: Math.PI / 2, - life: 4000, - opacity: 0.5, - fade: false, - startSize: 30, - endSize: 40, - beginColor: ex.Color.Red.clone(), - endColor: ex.Color.Blue.clone(), - graphic: null, - angularVelocity: 3, - randomRotation: false - }, - focus: null, - focusAccel: null, - emitterType: ex.EmitterType.Circle, - radius: 20, - random: new ex.Random(1337) + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GpuParticlesSpec/particles-wrapped.png'); }); - const parent = new ex.Actor({ - pos: ex.vec(100, 50), - width: 10, - height: 10 - }); - parent.addChild(emitter); + it('can be parented', async () => { + const emitter = new ex.ParticleEmitter({ + pos: new ex.Vector(0, 0), + width: 20, + height: 30, + isEmitting: true, + emitRate: 5, + particle: { + minSpeed: 100, + maxSpeed: 200, + acc: ex.Vector.Zero.clone(), + minAngle: 0, + maxAngle: Math.PI / 2, + life: 4000, + opacity: 0.5, + fade: false, + startSize: 30, + endSize: 40, + beginColor: ex.Color.Red.clone(), + endColor: ex.Color.Blue.clone(), + graphic: null, + angularVelocity: 3, + randomRotation: false + }, + focus: null, + focusAccel: null, + emitterType: ex.EmitterType.Circle, + radius: 20, + random: new ex.Random(1337) + }); - engine.backgroundColor = ex.Color.Transparent; - engine.add(emitter); + const parent = new ex.Actor({ + pos: ex.vec(100, 50), + width: 10, + height: 10 + }); + parent.addChild(emitter); - emitter.emitParticles(20); - engine.currentScene.update(engine, 100); - engine.currentScene.update(engine, 100); - engine.currentScene.update(engine, 100); - engine.currentScene.draw(engine.graphicsContext, 100); - engine.graphicsContext.flush(); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/ParticleSpec/parented.png'); + engine.backgroundColor = ex.Color.Transparent; + engine.add(emitter); + + emitter.emitParticles(20); + engine.currentScene.update(engine, 100); + engine.currentScene.update(engine, 100); + engine.currentScene.update(engine, 100); + engine.currentScene.draw(engine.graphicsContext, 100); + engine.graphicsContext.flush(); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/ParticleSpec/parented.png'); + }); }); it('can set the particle transform to local making particles children of the emitter', () => { diff --git a/src/spec/vitest/GraphicSpec.ts b/src/spec/vitest/GraphicSpec.ts index 0b980ad8..1fb02080 100644 --- a/src/spec/vitest/GraphicSpec.ts +++ b/src/spec/vitest/GraphicSpec.ts @@ -100,56 +100,58 @@ describe('A Graphic', () => { expect(sut.cloneGraphicOptions()).toEqual(originalOptions); }); - it('can draw a graphic implementation', async () => { - ctx.clear(); - const sut = new TestGraphic(); - sut.draw(ctx, 25, 25); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicSpec/base.png'); - }); - - it('has local bounds based on the width/height', () => { - const sut = new TestGraphic(); - expect(sut.localBounds).toEqual(new ex.BoundingBox(0, 0, sut.width, sut.height)); - }); - - it('can rotate a graphic implementation', async () => { - ctx.clear(); - const sut = new TestGraphic(); - sut.rotation = Math.PI / 4; - sut.draw(ctx, 25, 25); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicSpec/rotated.png'); - }); - - it('can flip a graphic implementation', async () => { - ctx.clear(); - const sut = new TestGraphic(); - sut.flipHorizontal = true; - sut.flipVertical = true; - sut.draw(ctx, 25, 25); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicSpec/flipped.png'); - }); - - it('can scale a graphic implementation', async () => { - ctx.clear(); - const sut = new TestGraphic(); - sut.scale = ex.vec(2, 2); - sut.draw(ctx, 25, 25); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicSpec/scaled.png'); - }); - - it('can set opacity on a graphic implementation', async () => { - ctx.clear(); - const sut = new TestGraphic(); - sut.opacity = 0.2; - sut.draw(ctx, 25, 25); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicSpec/opacity.png'); - }); - - it('can show a debug rect', async () => { - ctx.clear(); - const sut = new TestGraphic(); - sut.showDebug = true; - sut.draw(ctx, 25, 25); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicSpec/debug.png'); + describe('@visual', () => { + it('can draw a graphic implementation', async () => { + ctx.clear(); + const sut = new TestGraphic(); + sut.draw(ctx, 25, 25); + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicSpec/base.png'); + }); + + it('has local bounds based on the width/height', () => { + const sut = new TestGraphic(); + expect(sut.localBounds).toEqual(new ex.BoundingBox(0, 0, sut.width, sut.height)); + }); + + it('can rotate a graphic implementation', async () => { + ctx.clear(); + const sut = new TestGraphic(); + sut.rotation = Math.PI / 4; + sut.draw(ctx, 25, 25); + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicSpec/rotated.png'); + }); + + it('can flip a graphic implementation', async () => { + ctx.clear(); + const sut = new TestGraphic(); + sut.flipHorizontal = true; + sut.flipVertical = true; + sut.draw(ctx, 25, 25); + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicSpec/flipped.png'); + }); + + it('can scale a graphic implementation', async () => { + ctx.clear(); + const sut = new TestGraphic(); + sut.scale = ex.vec(2, 2); + sut.draw(ctx, 25, 25); + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicSpec/scaled.png'); + }); + + it('can set opacity on a graphic implementation', async () => { + ctx.clear(); + const sut = new TestGraphic(); + sut.opacity = 0.2; + sut.draw(ctx, 25, 25); + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicSpec/opacity.png'); + }); + + it('can show a debug rect', async () => { + ctx.clear(); + const sut = new TestGraphic(); + sut.showDebug = true; + sut.draw(ctx, 25, 25); + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicSpec/debug.png'); + }); }); }); diff --git a/src/spec/vitest/GraphicsGroupSpec.ts b/src/spec/vitest/GraphicsGroupSpec.ts index ea9a4de5..6e06d2e1 100644 --- a/src/spec/vitest/GraphicsGroupSpec.ts +++ b/src/spec/vitest/GraphicsGroupSpec.ts @@ -5,77 +5,79 @@ describe('A Graphics Group', () => { expect(ex.GraphicsGroup).toBeDefined(); }); - it('can be created and drawn', async () => { - const rect1 = new ex.Rectangle({ - width: 25, - height: 25, - color: ex.Color.Blue + describe('@visual', () => { + it('can be created and drawn', async () => { + const rect1 = new ex.Rectangle({ + width: 25, + height: 25, + color: ex.Color.Blue + }); + + const rect2 = new ex.Rectangle({ + width: 25, + height: 25, + color: ex.Color.Yellow + }); + + const group = new ex.GraphicsGroup({ + members: [ + { offset: ex.vec(0, 0), graphic: rect1 }, + { offset: ex.vec(25, 25), graphic: rect2 } + ] + }); + + expect(group.width).toBe(50); + expect(group.height).toBe(50); + expect(group.localBounds.width).toBe(50); + expect(group.localBounds.height).toBe(50); + + const canvasElement = document.createElement('canvas'); + canvasElement.width = 100; + canvasElement.height = 100; + const ctx = new ex.ExcaliburGraphicsContext2DCanvas({ canvasElement }); + + ctx.clear(); + group.draw(ctx, 25, 25); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsGroupSpec/graphics-group.png'); }); - const rect2 = new ex.Rectangle({ - width: 25, - height: 25, - color: ex.Color.Yellow + it('can be created and drawn without anchor', async () => { + const rect1 = new ex.Rectangle({ + width: 25, + height: 25, + color: ex.Color.Blue + }); + + const rect2 = new ex.Rectangle({ + width: 25, + height: 25, + color: ex.Color.Yellow + }); + + const group = new ex.GraphicsGroup({ + useAnchor: false, + members: [ + { offset: ex.vec(0, 0), graphic: rect1 }, + { offset: ex.vec(25, 25), graphic: rect2 } + ] + }); + + expect(group.width).toBe(50); + expect(group.height).toBe(50); + expect(group.localBounds.width).toBe(50); + expect(group.localBounds.height).toBe(50); + + const canvasElement = document.createElement('canvas'); + canvasElement.width = 100; + canvasElement.height = 100; + const ctx = new ex.ExcaliburGraphicsContext2DCanvas({ canvasElement }); + + ctx.clear(); + group.draw(ctx, 100, 100); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsGroupSpec/graphics-group-without-anchor.png'); }); - - const group = new ex.GraphicsGroup({ - members: [ - { offset: ex.vec(0, 0), graphic: rect1 }, - { offset: ex.vec(25, 25), graphic: rect2 } - ] - }); - - expect(group.width).toBe(50); - expect(group.height).toBe(50); - expect(group.localBounds.width).toBe(50); - expect(group.localBounds.height).toBe(50); - - const canvasElement = document.createElement('canvas'); - canvasElement.width = 100; - canvasElement.height = 100; - const ctx = new ex.ExcaliburGraphicsContext2DCanvas({ canvasElement }); - - ctx.clear(); - group.draw(ctx, 25, 25); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsGroupSpec/graphics-group.png'); - }); - - it('can be created and drawn without anchor', async () => { - const rect1 = new ex.Rectangle({ - width: 25, - height: 25, - color: ex.Color.Blue - }); - - const rect2 = new ex.Rectangle({ - width: 25, - height: 25, - color: ex.Color.Yellow - }); - - const group = new ex.GraphicsGroup({ - useAnchor: false, - members: [ - { offset: ex.vec(0, 0), graphic: rect1 }, - { offset: ex.vec(25, 25), graphic: rect2 } - ] - }); - - expect(group.width).toBe(50); - expect(group.height).toBe(50); - expect(group.localBounds.width).toBe(50); - expect(group.localBounds.height).toBe(50); - - const canvasElement = document.createElement('canvas'); - canvasElement.width = 100; - canvasElement.height = 100; - const ctx = new ex.ExcaliburGraphicsContext2DCanvas({ canvasElement }); - - ctx.clear(); - group.draw(ctx, 100, 100); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsGroupSpec/graphics-group-without-anchor.png'); }); it('can be cloned', () => { diff --git a/src/spec/vitest/GraphicsSystemSpec.ts b/src/spec/vitest/GraphicsSystemSpec.ts index 598af0ca..1223ae40 100644 --- a/src/spec/vitest/GraphicsSystemSpec.ts +++ b/src/spec/vitest/GraphicsSystemSpec.ts @@ -1,7 +1,7 @@ import * as ex from '@excalibur'; import { TransformComponent } from '@excalibur'; import { GraphicsComponent } from '../../engine/Graphics'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('A Graphics ECS System', () => { let entities: ex.Entity[]; @@ -49,71 +49,246 @@ describe('A Graphics ECS System', () => { expect(sut.sortedTransforms.map((t) => t.owner)).toEqual(entities.reverse()); }); - it('draws entities with transform and graphics components', async () => { - const world = engine.currentScene.world; - const sut = new ex.GraphicsSystem(world); - const offscreenSystem = new ex.OffscreenSystem(world); - engine.currentScene.camera.update(engine, 1); - engine.currentScene._initialize(engine); - engine.screen.setCurrentCamera(engine.currentScene.camera); - offscreenSystem.initialize(world, engine.currentScene); - sut.initialize(world, engine.currentScene); - - const rect = new ex.Rectangle({ - width: 25, - height: 25, - color: ex.Color.Yellow + describe('@visual', () => { + it('draws entities with transform and graphics components', async () => { + const world = engine.currentScene.world; + const sut = new ex.GraphicsSystem(world); + const offscreenSystem = new ex.OffscreenSystem(world); + engine.currentScene.camera.update(engine, 1); + engine.currentScene._initialize(engine); + engine.screen.setCurrentCamera(engine.currentScene.camera); + offscreenSystem.initialize(world, engine.currentScene); + sut.initialize(world, engine.currentScene); + + const rect = new ex.Rectangle({ + width: 25, + height: 25, + color: ex.Color.Yellow + }); + + const circle = new ex.Circle({ + radius: 13, + color: ex.Color.Green + }); + + const rect2 = new ex.Rectangle({ + width: 25, + height: 25, + color: ex.Color.Red + }); + + entities[0].get(TransformComponent).pos = ex.vec(25, 25); + entities[0].get(TransformComponent).rotation = Math.PI / 4; + entities[0].get(GraphicsComponent).use(rect); + + entities[1].get(TransformComponent).pos = ex.vec(75, 75); + entities[1].get(GraphicsComponent).use(circle); + + entities[2].get(TransformComponent).pos = ex.vec(75, 25); + entities[2].get(TransformComponent).scale = ex.vec(2, 2); + entities[2].get(GraphicsComponent).use(rect2); + + const offscreenRect = rect.clone(); + const offscreen = new ex.Entity().addComponent(new TransformComponent()).addComponent(new GraphicsComponent()); + offscreen.get(TransformComponent).pos = ex.vec(112.5, 112.5); + offscreen.get(GraphicsComponent).use(offscreenRect); + + vi.spyOn(rect, 'draw'); + vi.spyOn(circle, 'draw'); + vi.spyOn(rect2, 'draw'); + vi.spyOn(offscreenRect, 'draw'); + + entities.push(offscreen); + engine.graphicsContext.clear(); + entities.forEach((e) => offscreenSystem.query.checkAndAdd(e)); + entities.forEach((e) => sut.query.checkAndAdd(e)); + + offscreenSystem.update(); + + sut.preupdate(); + sut.update(1); + + expect(rect.draw).toHaveBeenCalled(); + expect(circle.draw).toHaveBeenCalled(); + expect(rect2.draw).toHaveBeenCalled(); + expect(offscreenRect.draw).not.toHaveBeenCalled(); + + engine.graphicsContext.flush(); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GraphicsSystemSpec/graphics-system.png'); }); - const circle = new ex.Circle({ - radius: 13, - color: ex.Color.Green + it('will multiply the opacity set on the context', async () => { + const world = engine.currentScene.world; + const sut = new ex.GraphicsSystem(world); + const offscreenSystem = new ex.OffscreenSystem(world); + engine.currentScene.camera.update(engine, 1); + engine.currentScene._initialize(engine); + engine.screen.setCurrentCamera(engine.currentScene.camera); + offscreenSystem.initialize(world, engine.currentScene); + sut.initialize(world, engine.currentScene); + + engine.graphicsContext.opacity = 0.5; + + const actor = new ex.Actor({ + x: 10, + y: 10, + height: 10, + width: 10, + color: ex.Color.Red + }); + actor.graphics.opacity = 0.5; + + sut.query.checkAndAdd(actor); + + offscreenSystem.query.checkAndAdd(actor); + offscreenSystem.update(); + + engine.graphicsContext.clear(); + sut.preupdate(); + sut.update(1); + + engine.graphicsContext.flush(); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GraphicsSystemSpec/graphics-context-opacity.png'); }); - const rect2 = new ex.Rectangle({ - width: 25, - height: 25, - color: ex.Color.Red + it('can flip graphics horizontally', async () => { + const world = engine.currentScene.world; + const sut = new ex.GraphicsSystem(world); + const offscreenSystem = new ex.OffscreenSystem(world); + engine.currentScene.camera.update(engine, 1); + engine.currentScene._initialize(engine); + engine.screen.setCurrentCamera(engine.currentScene.camera); + offscreenSystem.initialize(world, engine.currentScene); + sut.initialize(world, engine.currentScene); + + const sword = new ex.ImageSource('/src/spec/assets/images/GraphicsSystemSpec/sword.png'); + await sword.load(); + + const actor = new ex.Actor({ + x: 50, + y: 50, + height: 100, + width: 100 + }); + actor.graphics.use(sword.toSprite()); + actor.graphics.flipHorizontal = true; + + sut.query.checkAndAdd(actor); + + offscreenSystem.update(); + + engine.graphicsContext.clear(); + sut.preupdate(); + sut.update(1); + + engine.graphicsContext.flush(); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GraphicsSystemSpec/sword-flip-horizontal.png'); }); - entities[0].get(TransformComponent).pos = ex.vec(25, 25); - entities[0].get(TransformComponent).rotation = Math.PI / 4; - entities[0].get(GraphicsComponent).use(rect); - - entities[1].get(TransformComponent).pos = ex.vec(75, 75); - entities[1].get(GraphicsComponent).use(circle); - - entities[2].get(TransformComponent).pos = ex.vec(75, 25); - entities[2].get(TransformComponent).scale = ex.vec(2, 2); - entities[2].get(GraphicsComponent).use(rect2); - - const offscreenRect = rect.clone(); - const offscreen = new ex.Entity().addComponent(new TransformComponent()).addComponent(new GraphicsComponent()); - offscreen.get(TransformComponent).pos = ex.vec(112.5, 112.5); - offscreen.get(GraphicsComponent).use(offscreenRect); - - vi.spyOn(rect, 'draw'); - vi.spyOn(circle, 'draw'); - vi.spyOn(rect2, 'draw'); - vi.spyOn(offscreenRect, 'draw'); - - entities.push(offscreen); - engine.graphicsContext.clear(); - entities.forEach((e) => offscreenSystem.query.checkAndAdd(e)); - entities.forEach((e) => sut.query.checkAndAdd(e)); - - offscreenSystem.update(); - - sut.preupdate(); - sut.update(1); + it('can flip graphics vertically', async () => { + const world = engine.currentScene.world; + const sut = new ex.GraphicsSystem(world); + const offscreenSystem = new ex.OffscreenSystem(world); + engine.currentScene.camera.update(engine, 1); + engine.currentScene._initialize(engine); + engine.screen.setCurrentCamera(engine.currentScene.camera); + offscreenSystem.initialize(world, engine.currentScene); + sut.initialize(world, engine.currentScene); + + const sword = new ex.ImageSource('/src/spec/assets/images/GraphicsSystemSpec/sword.png'); + await sword.load(); + + const actor = new ex.Actor({ + x: 50, + y: 50, + height: 100, + width: 100 + }); + actor.graphics.use(sword.toSprite()); + actor.graphics.flipVertical = true; + + sut.query.checkAndAdd(actor); + + offscreenSystem.update(); + + engine.graphicsContext.clear(); + sut.preupdate(); + sut.update(1); + + engine.graphicsContext.flush(); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GraphicsSystemSpec/sword-flip-vertical.png'); + }); - expect(rect.draw).toHaveBeenCalled(); - expect(circle.draw).toHaveBeenCalled(); - expect(rect2.draw).toHaveBeenCalled(); - expect(offscreenRect.draw).not.toHaveBeenCalled(); + it('can flip graphics both horizontally and vertically', async () => { + const world = engine.currentScene.world; + const sut = new ex.GraphicsSystem(world); + const offscreenSystem = new ex.OffscreenSystem(world); + engine.currentScene.camera.update(engine, 1); + engine.currentScene._initialize(engine); + engine.screen.setCurrentCamera(engine.currentScene.camera); + offscreenSystem.initialize(world, engine.currentScene); + sut.initialize(world, engine.currentScene); + + const sword = new ex.ImageSource('/src/spec/assets/images/GraphicsSystemSpec/sword.png'); + await sword.load(); + + const actor = new ex.Actor({ + x: 50, + y: 50, + height: 100, + width: 100 + }); + actor.graphics.use(sword.toSprite()); + actor.graphics.flipVertical = true; + actor.graphics.flipHorizontal = true; + + sut.query.checkAndAdd(actor); + + offscreenSystem.update(); + + engine.graphicsContext.clear(); + sut.preupdate(); + sut.update(1); + + engine.graphicsContext.flush(); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GraphicsSystemSpec/sword-flip-both.png'); + }); - engine.graphicsContext.flush(); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GraphicsSystemSpec/graphics-system.png'); + it('can flip graphics both horizontally and vertically with an offset', async () => { + const world = engine.currentScene.world; + const sut = new ex.GraphicsSystem(world); + const offscreenSystem = new ex.OffscreenSystem(world); + engine.currentScene.camera.update(engine, 1); + engine.currentScene._initialize(engine); + engine.screen.setCurrentCamera(engine.currentScene.camera); + offscreenSystem.initialize(world, engine.currentScene); + sut.initialize(world, engine.currentScene); + + const sword = new ex.ImageSource('/src/spec/assets/images/GraphicsSystemSpec/sword.png'); + await sword.load(); + + const actor = new ex.Actor({ + x: 50, + y: 50, + height: 100, + width: 100 + }); + actor.graphics.use(sword.toSprite()); + actor.graphics.flipVertical = true; + actor.graphics.flipHorizontal = true; + actor.graphics.offset = ex.vec(25, 25); + + sut.query.checkAndAdd(actor); + + offscreenSystem.update(); + + engine.graphicsContext.clear(); + sut.preupdate(); + sut.update(1); + + engine.graphicsContext.flush(); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GraphicsSystemSpec/sword-flip-both-offset.png'); + }); }); it('will interpolate body graphics when fixed update is enabled', async () => { @@ -220,179 +395,6 @@ describe('A Graphics ECS System', () => { game.dispose(); }); - it('will multiply the opacity set on the context', async () => { - const world = engine.currentScene.world; - const sut = new ex.GraphicsSystem(world); - const offscreenSystem = new ex.OffscreenSystem(world); - engine.currentScene.camera.update(engine, 1); - engine.currentScene._initialize(engine); - engine.screen.setCurrentCamera(engine.currentScene.camera); - offscreenSystem.initialize(world, engine.currentScene); - sut.initialize(world, engine.currentScene); - - engine.graphicsContext.opacity = 0.5; - - const actor = new ex.Actor({ - x: 10, - y: 10, - height: 10, - width: 10, - color: ex.Color.Red - }); - actor.graphics.opacity = 0.5; - - sut.query.checkAndAdd(actor); - - offscreenSystem.query.checkAndAdd(actor); - offscreenSystem.update(); - - engine.graphicsContext.clear(); - sut.preupdate(); - sut.update(1); - - engine.graphicsContext.flush(); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GraphicsSystemSpec/graphics-context-opacity.png'); - }); - - it('can flip graphics horizontally', async () => { - const world = engine.currentScene.world; - const sut = new ex.GraphicsSystem(world); - const offscreenSystem = new ex.OffscreenSystem(world); - engine.currentScene.camera.update(engine, 1); - engine.currentScene._initialize(engine); - engine.screen.setCurrentCamera(engine.currentScene.camera); - offscreenSystem.initialize(world, engine.currentScene); - sut.initialize(world, engine.currentScene); - - const sword = new ex.ImageSource('/src/spec/assets/images/GraphicsSystemSpec/sword.png'); - await sword.load(); - - const actor = new ex.Actor({ - x: 50, - y: 50, - height: 100, - width: 100 - }); - actor.graphics.use(sword.toSprite()); - actor.graphics.flipHorizontal = true; - - sut.query.checkAndAdd(actor); - - offscreenSystem.update(); - - engine.graphicsContext.clear(); - sut.preupdate(); - sut.update(1); - - engine.graphicsContext.flush(); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GraphicsSystemSpec/sword-flip-horizontal.png'); - }); - - it('can flip graphics vertically', async () => { - const world = engine.currentScene.world; - const sut = new ex.GraphicsSystem(world); - const offscreenSystem = new ex.OffscreenSystem(world); - engine.currentScene.camera.update(engine, 1); - engine.currentScene._initialize(engine); - engine.screen.setCurrentCamera(engine.currentScene.camera); - offscreenSystem.initialize(world, engine.currentScene); - sut.initialize(world, engine.currentScene); - - const sword = new ex.ImageSource('/src/spec/assets/images/GraphicsSystemSpec/sword.png'); - await sword.load(); - - const actor = new ex.Actor({ - x: 50, - y: 50, - height: 100, - width: 100 - }); - actor.graphics.use(sword.toSprite()); - actor.graphics.flipVertical = true; - - sut.query.checkAndAdd(actor); - - offscreenSystem.update(); - - engine.graphicsContext.clear(); - sut.preupdate(); - sut.update(1); - - engine.graphicsContext.flush(); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GraphicsSystemSpec/sword-flip-vertical.png'); - }); - - it('can flip graphics both horizontally and vertically', async () => { - const world = engine.currentScene.world; - const sut = new ex.GraphicsSystem(world); - const offscreenSystem = new ex.OffscreenSystem(world); - engine.currentScene.camera.update(engine, 1); - engine.currentScene._initialize(engine); - engine.screen.setCurrentCamera(engine.currentScene.camera); - offscreenSystem.initialize(world, engine.currentScene); - sut.initialize(world, engine.currentScene); - - const sword = new ex.ImageSource('/src/spec/assets/images/GraphicsSystemSpec/sword.png'); - await sword.load(); - - const actor = new ex.Actor({ - x: 50, - y: 50, - height: 100, - width: 100 - }); - actor.graphics.use(sword.toSprite()); - actor.graphics.flipVertical = true; - actor.graphics.flipHorizontal = true; - - sut.query.checkAndAdd(actor); - - offscreenSystem.update(); - - engine.graphicsContext.clear(); - sut.preupdate(); - sut.update(1); - - engine.graphicsContext.flush(); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GraphicsSystemSpec/sword-flip-both.png'); - }); - - it('can flip graphics both horizontally and vertically with an offset', async () => { - const world = engine.currentScene.world; - const sut = new ex.GraphicsSystem(world); - const offscreenSystem = new ex.OffscreenSystem(world); - engine.currentScene.camera.update(engine, 1); - engine.currentScene._initialize(engine); - engine.screen.setCurrentCamera(engine.currentScene.camera); - offscreenSystem.initialize(world, engine.currentScene); - sut.initialize(world, engine.currentScene); - - const sword = new ex.ImageSource('/src/spec/assets/images/GraphicsSystemSpec/sword.png'); - await sword.load(); - - const actor = new ex.Actor({ - x: 50, - y: 50, - height: 100, - width: 100 - }); - actor.graphics.use(sword.toSprite()); - actor.graphics.flipVertical = true; - actor.graphics.flipHorizontal = true; - actor.graphics.offset = ex.vec(25, 25); - - sut.query.checkAndAdd(actor); - - offscreenSystem.update(); - - engine.graphicsContext.clear(); - sut.preupdate(); - sut.update(1); - - engine.graphicsContext.flush(); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/GraphicsSystemSpec/sword-flip-both-offset.png'); - }); - it('can add graphics+transform to a parent without a transform', () => { const world = engine.currentScene.world; const sut = new ex.GraphicsSystem(world); diff --git a/src/spec/vitest/ImageSourceSpec.ts b/src/spec/vitest/ImageSourceSpec.ts index fc8368df..7ffacf16 100644 --- a/src/spec/vitest/ImageSourceSpec.ts +++ b/src/spec/vitest/ImageSourceSpec.ts @@ -54,7 +54,7 @@ describe('A ImageSource', () => { xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" sodipodi:docname="resize-full.svg" inkscape:version="0.48.4 r9939" - xmlns="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" width="800px" height="800px" viewBox="0 0 1200 1200" enable-background="new 0 0 1200 1200" xml:space="preserve"> { - const sutCanvas = document.createElement('canvas')!; - sutCanvas.width = 100; - sutCanvas.height = 100; - const sutCtx = sutCanvas.getContext('2d')!; - sutCtx.fillStyle = ex.Color.Black.toRGBA(); - sutCtx.fillRect(20, 20, 50, 50); + describe('@visual', () => { + it('can be built from canvas elements', async () => { + const sutCanvas = document.createElement('canvas')!; + sutCanvas.width = 100; + sutCanvas.height = 100; + const sutCtx = sutCanvas.getContext('2d')!; + sutCtx.fillStyle = ex.Color.Black.toRGBA(); + sutCtx.fillRect(20, 20, 50, 50); - const img = ex.ImageSource.fromHtmlCanvasElement(sutCanvas); - const sprite = img.toSprite(); - await img.ready; + const img = ex.ImageSource.fromHtmlCanvasElement(sutCanvas); + const sprite = img.toSprite(); + await img.ready; - ctx.clear(); - sprite.draw(ctx, 0, 0); - ctx.flush(); + ctx.clear(); + sprite.draw(ctx, 0, 0); + ctx.flush(); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsImageSourceSpec/canvas-image.png'); - }); - - it('can be built from canvas elements with wrapping/filtering specified', async () => { - const sutCanvas = document.createElement('canvas')!; - sutCanvas.width = 100; - sutCanvas.height = 100; - const sutCtx = sutCanvas.getContext('2d')!; - sutCtx.fillStyle = ex.Color.Black.toRGBA(); - sutCtx.fillRect(20, 20, 50, 50); - - const img = ex.ImageSource.fromHtmlCanvasElement(sutCanvas, { - wrapping: ex.ImageWrapping.Repeat, - filtering: ex.ImageFiltering.Pixel + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsImageSourceSpec/canvas-image.png'); }); - const sprite = img.toSprite(); - await img.ready; - - expect(img.image.getAttribute(ex.ImageSourceAttributeConstants.Filtering)).toBe(ex.ImageFiltering.Pixel); - expect(img.image.getAttribute(ex.ImageSourceAttributeConstants.WrappingX)).toBe(ex.ImageWrapping.Repeat); - expect(img.image.getAttribute(ex.ImageSourceAttributeConstants.WrappingY)).toBe(ex.ImageWrapping.Repeat); - const img2 = ex.ImageSource.fromHtmlCanvasElement(sutCanvas, { - wrapping: { - x: ex.ImageWrapping.Repeat, - y: ex.ImageWrapping.Clamp - }, - filtering: ex.ImageFiltering.Blended - }); + it('can be built from canvas elements with wrapping/filtering specified', async () => { + const sutCanvas = document.createElement('canvas')!; + sutCanvas.width = 100; + sutCanvas.height = 100; + const sutCtx = sutCanvas.getContext('2d')!; + sutCtx.fillStyle = ex.Color.Black.toRGBA(); + sutCtx.fillRect(20, 20, 50, 50); + + const img = ex.ImageSource.fromHtmlCanvasElement(sutCanvas, { + wrapping: ex.ImageWrapping.Repeat, + filtering: ex.ImageFiltering.Pixel + }); + const sprite = img.toSprite(); + await img.ready; + + expect(img.image.getAttribute(ex.ImageSourceAttributeConstants.Filtering)).toBe(ex.ImageFiltering.Pixel); + expect(img.image.getAttribute(ex.ImageSourceAttributeConstants.WrappingX)).toBe(ex.ImageWrapping.Repeat); + expect(img.image.getAttribute(ex.ImageSourceAttributeConstants.WrappingY)).toBe(ex.ImageWrapping.Repeat); + + const img2 = ex.ImageSource.fromHtmlCanvasElement(sutCanvas, { + wrapping: { + x: ex.ImageWrapping.Repeat, + y: ex.ImageWrapping.Clamp + }, + filtering: ex.ImageFiltering.Blended + }); - expect(img2.image.getAttribute(ex.ImageSourceAttributeConstants.Filtering)).toBe(ex.ImageFiltering.Blended); - expect(img2.image.getAttribute(ex.ImageSourceAttributeConstants.WrappingX)).toBe(ex.ImageWrapping.Repeat); - expect(img2.image.getAttribute(ex.ImageSourceAttributeConstants.WrappingY)).toBe(ex.ImageWrapping.Clamp); + expect(img2.image.getAttribute(ex.ImageSourceAttributeConstants.Filtering)).toBe(ex.ImageFiltering.Blended); + expect(img2.image.getAttribute(ex.ImageSourceAttributeConstants.WrappingX)).toBe(ex.ImageWrapping.Repeat); + expect(img2.image.getAttribute(ex.ImageSourceAttributeConstants.WrappingY)).toBe(ex.ImageWrapping.Clamp); - ctx.clear(); - sprite.draw(ctx, 0, 0); - ctx.flush(); + ctx.clear(); + sprite.draw(ctx, 0, 0); + ctx.flush(); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsImageSourceSpec/canvas-image.png'); + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsImageSourceSpec/canvas-image.png'); + }); }); }); diff --git a/src/spec/vitest/InputMapperSpec.ts b/src/spec/vitest/InputMapperSpec.ts index 67f776cb..55d07ad1 100644 --- a/src/spec/vitest/InputMapperSpec.ts +++ b/src/spec/vitest/InputMapperSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('An InputMapper', () => { it('exists', () => { diff --git a/src/spec/vitest/IsometricMapSpec.ts b/src/spec/vitest/IsometricMapSpec.ts index d2d936ed..27f61bf4 100644 --- a/src/spec/vitest/IsometricMapSpec.ts +++ b/src/spec/vitest/IsometricMapSpec.ts @@ -1,6 +1,6 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('A IsometricMap', () => { it('exists', () => { @@ -19,115 +19,117 @@ describe('A IsometricMap', () => { expect(sut).toBeDefined(); }); - it('can be drawn', async () => { - const engine = TestUtils.engine({}, ['suppress-obsolete-message']); - // engine.toggleDebug(); - const clock = engine.clock as ex.TestClock; - const image = new ex.ImageSource('/src/spec/assets/images/IsometricMapSpec/tile.png'); - await image.load(); - const sprite = image.toSprite(); - await TestUtils.runToReady(engine); - - const sut = new ex.IsometricMap({ - pos: ex.vec(250, 10), - tileWidth: 32, - tileHeight: 16, - columns: 15, - rows: 15 + describe('@visual', () => { + it('can be drawn', async () => { + const engine = TestUtils.engine({}, ['suppress-obsolete-message']); + // engine.toggleDebug(); + const clock = engine.clock as ex.TestClock; + const image = new ex.ImageSource('/src/spec/assets/images/IsometricMapSpec/tile.png'); + await image.load(); + const sprite = image.toSprite(); + await TestUtils.runToReady(engine); + + const sut = new ex.IsometricMap({ + pos: ex.vec(250, 10), + tileWidth: 32, + tileHeight: 16, + columns: 15, + rows: 15 + }); + + sut.tiles.forEach((tile) => tile.addGraphic(sprite)); + + engine.add(sut); + clock.step(100); + + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/IsometricMapSpec/map.png'); + engine.dispose(); }); - sut.tiles.forEach((tile) => tile.addGraphic(sprite)); - - engine.add(sut); - clock.step(100); - - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/IsometricMapSpec/map.png'); - engine.dispose(); - }); - - it('can be drawn from the top', async () => { - const engine = TestUtils.engine({}, ['suppress-obsolete-message']); - // engine.toggleDebug(); - const clock = engine.clock as ex.TestClock; - const image = new ex.ImageSource('/src/spec/assets/images/IsometricMapSpec/cube.png'); - await image.load(); - const sprite = image.toSprite(); - await TestUtils.runToReady(engine); - - const sut = new ex.IsometricMap({ - pos: ex.vec(250, 10), - renderFromTopOfGraphic: true, - tileWidth: 32, - tileHeight: 16, - columns: 15, - rows: 15 + it('can be drawn from the top', async () => { + const engine = TestUtils.engine({}, ['suppress-obsolete-message']); + // engine.toggleDebug(); + const clock = engine.clock as ex.TestClock; + const image = new ex.ImageSource('/src/spec/assets/images/IsometricMapSpec/cube.png'); + await image.load(); + const sprite = image.toSprite(); + await TestUtils.runToReady(engine); + + const sut = new ex.IsometricMap({ + pos: ex.vec(250, 10), + renderFromTopOfGraphic: true, + tileWidth: 32, + tileHeight: 16, + columns: 15, + rows: 15 + }); + + sut.tiles.forEach((tile) => tile.addGraphic(sprite)); + + engine.add(sut); + clock.step(100); + + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/IsometricMapSpec/cube-map-top.png'); + engine.dispose(); }); - sut.tiles.forEach((tile) => tile.addGraphic(sprite)); - - engine.add(sut); - clock.step(100); - - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/IsometricMapSpec/cube-map-top.png'); - engine.dispose(); - }); - - it('can be drawn from the bottom', async () => { - const engine = TestUtils.engine({}, ['suppress-obsolete-message']); - // engine.toggleDebug(); - const clock = engine.clock as ex.TestClock; - const image = new ex.ImageSource('/src/spec/assets/images/IsometricMapSpec/cube.png'); - await image.load(); - const sprite = image.toSprite(); - await TestUtils.runToReady(engine); - - const sut = new ex.IsometricMap({ - pos: ex.vec(250, 10), - tileWidth: 32, - tileHeight: 16, - columns: 15, - rows: 15 + it('can be drawn from the bottom', async () => { + const engine = TestUtils.engine({}, ['suppress-obsolete-message']); + // engine.toggleDebug(); + const clock = engine.clock as ex.TestClock; + const image = new ex.ImageSource('/src/spec/assets/images/IsometricMapSpec/cube.png'); + await image.load(); + const sprite = image.toSprite(); + await TestUtils.runToReady(engine); + + const sut = new ex.IsometricMap({ + pos: ex.vec(250, 10), + tileWidth: 32, + tileHeight: 16, + columns: 15, + rows: 15 + }); + + sut.tiles.forEach((tile) => tile.addGraphic(sprite)); + + engine.add(sut); + clock.step(100); + + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/IsometricMapSpec/cube-map-bottom.png'); + engine.dispose(); }); - sut.tiles.forEach((tile) => tile.addGraphic(sprite)); - - engine.add(sut); - clock.step(100); - - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/IsometricMapSpec/cube-map-bottom.png'); - engine.dispose(); - }); - - it('can be debug drawn', async () => { - const engine = TestUtils.engine({}, ['suppress-obsolete-message']); - engine.toggleDebug(); - engine.debug.entity.showName = false; - engine.debug.entity.showId = false; - engine.debug.graphics.showBounds = false; - engine.debug.transform.showPosition = true; - engine.debug.isometric.showGrid = true; - engine.debug.isometric.showPosition = true; - const clock = engine.clock as ex.TestClock; - const image = new ex.ImageSource('/src/spec/assets/images/IsometricMapSpec/cube.png'); - await image.load(); - const sprite = image.toSprite(); - await TestUtils.runToReady(engine); - - const sut = new ex.IsometricMap({ - pos: ex.vec(250, 10), - tileWidth: 32, - tileHeight: 16, - columns: 15, - rows: 15 + it('can be debug drawn', async () => { + const engine = TestUtils.engine({}, ['suppress-obsolete-message']); + engine.toggleDebug(); + engine.debug.entity.showName = false; + engine.debug.entity.showId = false; + engine.debug.graphics.showBounds = false; + engine.debug.transform.showPosition = true; + engine.debug.isometric.showGrid = true; + engine.debug.isometric.showPosition = true; + const clock = engine.clock as ex.TestClock; + const image = new ex.ImageSource('/src/spec/assets/images/IsometricMapSpec/cube.png'); + await image.load(); + const sprite = image.toSprite(); + await TestUtils.runToReady(engine); + + const sut = new ex.IsometricMap({ + pos: ex.vec(250, 10), + tileWidth: 32, + tileHeight: 16, + columns: 15, + rows: 15 + }); + + sut.tiles.forEach((tile) => tile.addGraphic(sprite)); + + engine.add(sut); + clock.step(100); + + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/IsometricMapSpec/cube-map-debug.png'); + engine.dispose(); }); - - sut.tiles.forEach((tile) => tile.addGraphic(sprite)); - - engine.add(sut); - clock.step(100); - - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/IsometricMapSpec/cube-map-debug.png'); - engine.dispose(); }); it('can find a tile coordinate from a world position', async () => { diff --git a/src/spec/vitest/KeyInputSpec.ts b/src/spec/vitest/KeyInputSpec.ts index ac0dcae6..ab3bd286 100644 --- a/src/spec/vitest/KeyInputSpec.ts +++ b/src/spec/vitest/KeyInputSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { Mocks } from './util/Mocks'; +import { Mocks } from '../__util__/Mocks'; describe('A keyboard', () => { let mockWindow = null; diff --git a/src/spec/vitest/LabelSpec.ts b/src/spec/vitest/LabelSpec.ts index cdc6c3a7..559934e0 100644 --- a/src/spec/vitest/LabelSpec.ts +++ b/src/spec/vitest/LabelSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('A label', () => { let label: ex.Label; diff --git a/src/spec/LinesSpec.ts b/src/spec/vitest/LineSegmentSpec.ts similarity index 99% rename from src/spec/LinesSpec.ts rename to src/spec/vitest/LineSegmentSpec.ts index 893909ad..a82bb413 100644 --- a/src/spec/LinesSpec.ts +++ b/src/spec/vitest/LineSegmentSpec.ts @@ -1,4 +1,4 @@ -import * as ex from 'excalibur'; +import * as ex from '@excalibur'; describe('Lines', () => { it('exist', () => { diff --git a/src/spec/vitest/LineSpec.ts b/src/spec/vitest/LineSpec.ts index e7542248..9b5f8f15 100644 --- a/src/spec/vitest/LineSpec.ts +++ b/src/spec/vitest/LineSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('A Line', () => { it('exists', () => { @@ -67,55 +67,57 @@ describe('A Line', () => { expect(sut.height).toBe(4); }); - it('can draw a line', async () => { - const sut = new ex.Line({ - start: ex.vec(0, 0), - end: ex.vec(50, 50), - color: ex.Color.Green, - thickness: 5 + describe('@visual', () => { + it('can draw a line', async () => { + const sut = new ex.Line({ + start: ex.vec(0, 0), + end: ex.vec(50, 50), + color: ex.Color.Green, + thickness: 5 + }); + + const canvasElement = document.createElement('canvas'); + canvasElement.width = 100; + canvasElement.height = 100; + const ctx = new ex.ExcaliburGraphicsContextWebGL({ canvasElement, snapToPixel: false }); + + ctx.clear(); + sut.draw(ctx, 0, 0); + ctx.flush(); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/LineSpec/line.png'); }); - const canvasElement = document.createElement('canvas'); - canvasElement.width = 100; - canvasElement.height = 100; - const ctx = new ex.ExcaliburGraphicsContextWebGL({ canvasElement, snapToPixel: false }); + it('can draw a line when added to a graphics component', async () => { + const game = TestUtils.engine({ + width: 100, + height: 100, + backgroundColor: ex.Color.ExcaliburBlue + }); - ctx.clear(); - sut.draw(ctx, 0, 0); - ctx.flush(); + const testClock = game.clock as ex.TestClock; - await expect(canvasElement).toEqualImage('/src/spec/assets/images/LineSpec/line.png'); - }); + await TestUtils.runToReady(game); - it('can draw a line when added to a graphics component', async () => { - const game = TestUtils.engine({ - width: 100, - height: 100, - backgroundColor: ex.Color.ExcaliburBlue - }); + const sut = new ex.Line({ + start: ex.vec(0, 0), + end: ex.vec(50, 50), + color: ex.Color.Green, + thickness: 5 + }); - const testClock = game.clock as ex.TestClock; + const actor = new ex.Actor({ + pos: ex.vec(0, 0) + }); + actor.graphics.anchor = ex.Vector.Zero; + actor.graphics.use(sut); + game.add(actor); - await TestUtils.runToReady(game); + testClock.step(16); - const sut = new ex.Line({ - start: ex.vec(0, 0), - end: ex.vec(50, 50), - color: ex.Color.Green, - thickness: 5 - }); + await expect(game.canvas).toEqualImage('/src/spec/assets/images/LineSpec/line.png'); - const actor = new ex.Actor({ - pos: ex.vec(0, 0) + game.dispose(); }); - actor.graphics.anchor = ex.Vector.Zero; - actor.graphics.use(sut); - game.add(actor); - - testClock.step(16); - - await expect(game.canvas).toEqualImage('/src/spec/assets/images/LineSpec/line.png'); - - game.dispose(); }); }); diff --git a/src/spec/vitest/LoaderSpec.ts b/src/spec/vitest/LoaderSpec.ts index a545f639..dd0eb13a 100644 --- a/src/spec/vitest/LoaderSpec.ts +++ b/src/spec/vitest/LoaderSpec.ts @@ -1,6 +1,6 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('A loader', () => { let engine: ex.Engine; @@ -366,33 +366,35 @@ describe('A loader', () => { }); })); - it('should not show the play button when suppressPlayButton is turned on', () => - new Promise((done, reject) => { - reset(); - engine = TestUtils.engine({ suppressPlayButton: false }); - engine.currentScene.add( - new ex.Actor({ - pos: new ex.Vector(250, 250), - width: 20, - height: 20, - color: ex.Color.Red - }) - ); - - const testClock = engine.clock as ex.TestClock; - const loader = new ex.Loader([new ex.ImageSource('/src/spec/assets/images/SpriteSpec/icon.png')]); - loader.suppressPlayButton = true; - - TestUtils.runToReady(engine, loader).then(() => { - // With suppress play there is another 500 ms delay in engine load() - testClock.step(1); - engine.graphicsContext.flush(); - expect(engine.canvas) - .toEqualImage('/src/spec/assets/images/EngineSpec/engine-suppress-play.png') - .then(() => { - done(); + describe('@visual', () => { + it('should not show the play button when suppressPlayButton is turned on', () => + new Promise((done, reject) => { + reset(); + engine = TestUtils.engine({ suppressPlayButton: false }); + engine.currentScene.add( + new ex.Actor({ + pos: new ex.Vector(250, 250), + width: 20, + height: 20, + color: ex.Color.Red }) - .catch(reject); - }); - })); + ); + + const testClock = engine.clock as ex.TestClock; + const loader = new ex.Loader([new ex.ImageSource('/src/spec/assets/images/SpriteSpec/icon.png')]); + loader.suppressPlayButton = true; + + TestUtils.runToReady(engine, loader).then(() => { + // With suppress play there is another 500 ms delay in engine load() + testClock.step(1); + engine.graphicsContext.flush(); + expect(engine.canvas) + .toEqualImage('/src/spec/assets/images/EngineSpec/engine-suppress-play.png') + .then(() => { + done(); + }) + .catch(reject); + }); + })); + }); }); diff --git a/src/spec/vitest/MaterialRendererSpec.ts b/src/spec/vitest/MaterialRendererSpec.ts index ef39ec07..ceb252d7 100644 --- a/src/spec/vitest/MaterialRendererSpec.ts +++ b/src/spec/vitest/MaterialRendererSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('A Material', () => { let engine: ex.Engine; @@ -50,21 +50,22 @@ describe('A Material', () => { expect(() => material.use()).not.toThrow(); }); - it('can be created with a custom fragment shader', async () => { - const canvas = document.createElement('canvas'); - canvas.width = 100; - canvas.height = 100; - const graphicsContext = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvas, - backgroundColor: ex.Color.Black, - antialiasing: false, - snapToPixel: true - }); - const material = new ex.Material({ - name: 'test', - graphicsContext, - color: ex.Color.Red, - fragmentSource: `#version 300 es + describe('@visual', () => { + it('can be created with a custom fragment shader', async () => { + const canvas = document.createElement('canvas'); + canvas.width = 100; + canvas.height = 100; + const graphicsContext = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvas, + backgroundColor: ex.Color.Black, + antialiasing: false, + snapToPixel: true + }); + const material = new ex.Material({ + name: 'test', + graphicsContext, + color: ex.Color.Red, + fragmentSource: `#version 300 es precision mediump float; // UV coord in vec2 v_uv; @@ -79,38 +80,38 @@ describe('A Material', () => { color.a = color.a * u_opacity; fragColor = color * u_color; }` - }); - - const tex = new ex.ImageSource('/src/spec/assets/images/MaterialRendererSpec/sword.png'); - await tex.load(); + }); - graphicsContext.clear(); - graphicsContext.save(); - graphicsContext.material = material; - graphicsContext.drawImage(tex.image, 0, 0); - graphicsContext.flush(); - graphicsContext.restore(); + const tex = new ex.ImageSource('/src/spec/assets/images/MaterialRendererSpec/sword.png'); + await tex.load(); - expect(graphicsContext.material).toBe(null); - await expect(canvas).toEqualImage('/src/spec/assets/images/MaterialRendererSpec/material.png'); - graphicsContext.dispose(); - }); + graphicsContext.clear(); + graphicsContext.save(); + graphicsContext.material = material; + graphicsContext.drawImage(tex.image, 0, 0); + graphicsContext.flush(); + graphicsContext.restore(); - it('can draw the screen texture', async () => { - const canvas = document.createElement('canvas'); - canvas.width = 100; - canvas.height = 100; - const context = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvas, - backgroundColor: ex.Color.ExcaliburBlue, - antialiasing: false, - snapToPixel: true + expect(graphicsContext.material).toBe(null); + await expect(canvas).toEqualImage('/src/spec/assets/images/MaterialRendererSpec/material.png'); + graphicsContext.dispose(); }); - const material = context.createMaterial({ - name: 'test', - color: ex.Color.Red, - fragmentSource: `#version 300 es + it('can draw the screen texture', async () => { + const canvas = document.createElement('canvas'); + canvas.width = 100; + canvas.height = 100; + const context = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvas, + backgroundColor: ex.Color.ExcaliburBlue, + antialiasing: false, + snapToPixel: true + }); + + const material = context.createMaterial({ + name: 'test', + color: ex.Color.Red, + fragmentSource: `#version 300 es precision mediump float; // UV coord in vec2 v_uv; @@ -122,38 +123,38 @@ describe('A Material', () => { void main() { fragColor = texture(u_screen_texture, v_screenuv) * texture(u_graphic, v_uv); }` - }); + }); - const tex = new ex.ImageSource('/src/spec/assets/images/MaterialRendererSpec/sword.png'); - await tex.load(); + const tex = new ex.ImageSource('/src/spec/assets/images/MaterialRendererSpec/sword.png'); + await tex.load(); - context.clear(); - context.save(); - context.material = material; - context.drawImage(tex.image, 0, 0); - context.flush(); - context.restore(); - - expect(context.material).toBe(null); - await expect(canvas).toEqualImage('/src/spec/assets/images/MaterialRendererSpec/multiply-comp.png'); - context.dispose(); - }); + context.clear(); + context.save(); + context.material = material; + context.drawImage(tex.image, 0, 0); + context.flush(); + context.restore(); - it('can update uniforms with the .update()', async () => { - const canvas = document.createElement('canvas'); - canvas.width = 100; - canvas.height = 100; - const context = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvas, - backgroundColor: ex.Color.ExcaliburBlue, - antialiasing: false, - snapToPixel: true + expect(context.material).toBe(null); + await expect(canvas).toEqualImage('/src/spec/assets/images/MaterialRendererSpec/multiply-comp.png'); + context.dispose(); }); - const material = context.createMaterial({ - name: 'test', - color: ex.Color.Red, - fragmentSource: `#version 300 es + it('can update uniforms with the .update()', async () => { + const canvas = document.createElement('canvas'); + canvas.width = 100; + canvas.height = 100; + const context = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvas, + backgroundColor: ex.Color.ExcaliburBlue, + antialiasing: false, + snapToPixel: true + }); + + const material = context.createMaterial({ + name: 'test', + color: ex.Color.Red, + fragmentSource: `#version 300 es precision mediump float; // UV coord in vec2 v_uv; @@ -166,38 +167,38 @@ describe('A Material', () => { void main() { fragColor = texture(u_screen_texture, v_screenuv) * texture(u_graphic, v_uv) - customcolor; }` + }); + + const tex = new ex.ImageSource('/src/spec/assets/images/MaterialRendererSpec/sword.png'); + await tex.load(); + + context.clear(); + context.save(); + context.material = material; + material.update((shader) => { + shader.setUniformFloatColor('customcolor', ex.Color.Red); + }); + context.drawImage(tex.image, 0, 0); + context.flush(); + context.restore(); + + expect(context.material).toBe(null); + await expect(canvas).toEqualImage('/src/spec/assets/images/MaterialRendererSpec/update-uniform.png'); }); - const tex = new ex.ImageSource('/src/spec/assets/images/MaterialRendererSpec/sword.png'); - await tex.load(); - - context.clear(); - context.save(); - context.material = material; - material.update((shader) => { - shader.setUniformFloatColor('customcolor', ex.Color.Red); - }); - context.drawImage(tex.image, 0, 0); - context.flush(); - context.restore(); - - expect(context.material).toBe(null); - await expect(canvas).toEqualImage('/src/spec/assets/images/MaterialRendererSpec/update-uniform.png'); - }); - - it('can be created with a custom fragment shader with the graphics component', async () => { - const engine = TestUtils.engine({ - width: 100, - height: 100, - antialiasing: false, - snapToPixel: true - }); - const graphicsContext = engine.graphicsContext as ex.ExcaliburGraphicsContextWebGL; - const material = new ex.Material({ - name: 'test', - graphicsContext, - color: ex.Color.Red, - fragmentSource: `#version 300 es + it('can be created with a custom fragment shader with the graphics component', async () => { + const engine = TestUtils.engine({ + width: 100, + height: 100, + antialiasing: false, + snapToPixel: true + }); + const graphicsContext = engine.graphicsContext as ex.ExcaliburGraphicsContextWebGL; + const material = new ex.Material({ + name: 'test', + graphicsContext, + color: ex.Color.Red, + fragmentSource: `#version 300 es precision mediump float; // UV coord in vec2 v_uv; @@ -212,121 +213,121 @@ describe('A Material', () => { color.a = color.a * u_opacity; fragColor = color * u_color; }` - }); - - const tex = new ex.ImageSource('/src/spec/assets/images/MaterialRendererSpec/sword.png'); + }); - const loader = new ex.Loader([tex]); + const tex = new ex.ImageSource('/src/spec/assets/images/MaterialRendererSpec/sword.png'); - await TestUtils.runToReady(engine, loader); + const loader = new ex.Loader([tex]); - const actor = new ex.Actor({ - x: 50, - y: 50, - width: 100, - height: 100 - }); - actor.graphics.use(tex.toSprite()); - actor.graphics.material = material; + await TestUtils.runToReady(engine, loader); - graphicsContext.clear(); - engine.currentScene.add(actor); - engine.currentScene.draw(graphicsContext, 100); - graphicsContext.flush(); + const actor = new ex.Actor({ + x: 50, + y: 50, + width: 100, + height: 100 + }); + actor.graphics.use(tex.toSprite()); + actor.graphics.material = material; - expect(graphicsContext.material).toBe(null); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/MaterialRendererSpec/material-component.png'); - engine.dispose(); - }); + graphicsContext.clear(); + engine.currentScene.add(actor); + engine.currentScene.draw(graphicsContext, 100); + graphicsContext.flush(); - it('can be draw multiple materials', async () => { - const engine = TestUtils.engine({ - width: 100, - height: 100, - antialiasing: false, - snapToPixel: true + expect(graphicsContext.material).toBe(null); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/MaterialRendererSpec/material-component.png'); + engine.dispose(); }); - const graphicsContext = engine.graphicsContext; - const material1 = new ex.Material({ - name: 'material1', - graphicsContext, - color: ex.Color.Red, - fragmentSource: `#version 300 es + + it('can be draw multiple materials', async () => { + const engine = TestUtils.engine({ + width: 100, + height: 100, + antialiasing: false, + snapToPixel: true + }); + const graphicsContext = engine.graphicsContext; + const material1 = new ex.Material({ + name: 'material1', + graphicsContext, + color: ex.Color.Red, + fragmentSource: `#version 300 es precision mediump float; uniform vec4 u_color; out vec4 fragColor; void main() { fragColor = u_color; }` - }); + }); - const material2 = new ex.Material({ - name: 'material2', - graphicsContext, - color: ex.Color.Blue, - fragmentSource: `#version 300 es + const material2 = new ex.Material({ + name: 'material2', + graphicsContext, + color: ex.Color.Blue, + fragmentSource: `#version 300 es precision mediump float; uniform vec4 u_color; out vec4 fragColor; void main() { fragColor = u_color; }` + }); + + const tex = new ex.ImageSource('/src/spec/assets/images/MaterialRendererSpec/sword.png'); + + const loader = new ex.Loader([tex]); + + await TestUtils.runToReady(engine, loader); + + const actor1 = new ex.Actor({ + x: 0, + y: 0, + width: 100, + height: 100 + }); + actor1.graphics.use(tex.toSprite()); + actor1.graphics.material = material1; + + const actor2 = new ex.Actor({ + x: 100, + y: 100, + width: 100, + height: 100 + }); + actor2.graphics.use(tex.toSprite()); + actor2.graphics.material = material2; + + graphicsContext.clear(); + engine.currentScene.add(actor1); + engine.currentScene.add(actor2); + engine.currentScene.draw(graphicsContext, 100); + graphicsContext.flush(); + + expect(graphicsContext.material).toBe(null); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/MaterialRendererSpec/multi-mat.png'); + + engine.dispose(); }); - const tex = new ex.ImageSource('/src/spec/assets/images/MaterialRendererSpec/sword.png'); - - const loader = new ex.Loader([tex]); - - await TestUtils.runToReady(engine, loader); - - const actor1 = new ex.Actor({ - x: 0, - y: 0, - width: 100, - height: 100 - }); - actor1.graphics.use(tex.toSprite()); - actor1.graphics.material = material1; - - const actor2 = new ex.Actor({ - x: 100, - y: 100, - width: 100, - height: 100 - }); - actor2.graphics.use(tex.toSprite()); - actor2.graphics.material = material2; - - graphicsContext.clear(); - engine.currentScene.add(actor1); - engine.currentScene.add(actor2); - engine.currentScene.draw(graphicsContext, 100); - graphicsContext.flush(); - - expect(graphicsContext.material).toBe(null); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/MaterialRendererSpec/multi-mat.png'); - - engine.dispose(); - }); - - it('will allow addition images', async () => { - const canvas = document.createElement('canvas'); - canvas.width = 100; - canvas.height = 100; - const graphicsContext = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvas, - backgroundColor: ex.Color.Black, - antialiasing: false, - snapToPixel: true - }); - - const stars = new ex.ImageSource('/src/spec/assets/images/MaterialRendererSpec/stars.png'); - await stars.load(); - - const material = new ex.Material({ - name: 'test', - graphicsContext, - fragmentSource: `#version 300 es + it('will allow addition images', async () => { + const canvas = document.createElement('canvas'); + canvas.width = 100; + canvas.height = 100; + const graphicsContext = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvas, + backgroundColor: ex.Color.Black, + antialiasing: false, + snapToPixel: true + }); + + const stars = new ex.ImageSource('/src/spec/assets/images/MaterialRendererSpec/stars.png'); + await stars.load(); + + const material = new ex.Material({ + name: 'test', + graphicsContext, + fragmentSource: `#version 300 es precision mediump float; // UV coord in vec2 v_uv; @@ -342,45 +343,45 @@ describe('A Material', () => { color.a = color.a * u_opacity; fragColor = color * u_color; }`, - images: { - u_additional: stars - } - }); - - const tex = new ex.ImageSource('/src/spec/assets/images/MaterialRendererSpec/sword.png'); - await tex.load(); - - graphicsContext.clear(); - graphicsContext.save(); - graphicsContext.material = material; - graphicsContext.drawImage(tex.image, 0, 0); - graphicsContext.flush(); - graphicsContext.restore(); - - expect(graphicsContext.material).toBe(null); - await expect(canvas).toEqualImage('/src/spec/assets/images/MaterialRendererSpec/additional.png'); - }); - - it('will log a warning if you exceed you texture slots', () => { - const logger = ex.Logger.getInstance(); - vi.spyOn(logger, 'warn'); - - const canvas = document.createElement('canvas'); - canvas.width = 100; - canvas.height = 100; - const graphicsContext = new ex.ExcaliburGraphicsContextWebGL({ - canvasElement: canvas, - backgroundColor: ex.Color.Black, - antialiasing: false, - snapToPixel: true + images: { + u_additional: stars + } + }); + + const tex = new ex.ImageSource('/src/spec/assets/images/MaterialRendererSpec/sword.png'); + await tex.load(); + + graphicsContext.clear(); + graphicsContext.save(); + graphicsContext.material = material; + graphicsContext.drawImage(tex.image, 0, 0); + graphicsContext.flush(); + graphicsContext.restore(); + + expect(graphicsContext.material).toBe(null); + await expect(canvas).toEqualImage('/src/spec/assets/images/MaterialRendererSpec/additional.png'); }); - const stars = new ex.ImageSource('/src/spec/assets/images/MaterialRendererSpec/stars.png'); - - const material = new ex.Material({ - name: 'test', - graphicsContext, - fragmentSource: `#version 300 es + it('will log a warning if you exceed you texture slots', () => { + const logger = ex.Logger.getInstance(); + vi.spyOn(logger, 'warn'); + + const canvas = document.createElement('canvas'); + canvas.width = 100; + canvas.height = 100; + const graphicsContext = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvas, + backgroundColor: ex.Color.Black, + antialiasing: false, + snapToPixel: true + }); + + const stars = new ex.ImageSource('/src/spec/assets/images/MaterialRendererSpec/stars.png'); + + const material = new ex.Material({ + name: 'test', + graphicsContext, + fragmentSource: `#version 300 es precision mediump float; // UV coord in vec2 v_uv; @@ -396,49 +397,50 @@ describe('A Material', () => { color.a = color.a * u_opacity; fragColor = color * u_color; }`, - images: { - u_additional: stars, - u_additional1: stars, - u_additional2: stars, - u_additional3: stars, - u_additional4: stars, - u_additional5: stars, - u_additional6: stars, - u_additional7: stars, - u_additional8: stars, - u_additional9: stars, - u_additional10: stars, - u_additional11: stars, - u_additional12: stars, - u_additional13: stars, - u_additional14: stars, - u_additional15: stars, - u_additional16: stars, - u_additional17: stars, - u_additional18: stars, - u_additional19: stars, - u_additional20: stars, - u_additional21: stars, - u_additional22: stars, - u_additional23: stars, - u_additional24: stars, - u_additional25: stars, - u_additional26: stars, - u_additional27: stars, - u_additional28: stars, - u_additional29: stars, - u_additional30: stars, - u_additional31: stars, - u_additional32: stars - } + images: { + u_additional: stars, + u_additional1: stars, + u_additional2: stars, + u_additional3: stars, + u_additional4: stars, + u_additional5: stars, + u_additional6: stars, + u_additional7: stars, + u_additional8: stars, + u_additional9: stars, + u_additional10: stars, + u_additional11: stars, + u_additional12: stars, + u_additional13: stars, + u_additional14: stars, + u_additional15: stars, + u_additional16: stars, + u_additional17: stars, + u_additional18: stars, + u_additional19: stars, + u_additional20: stars, + u_additional21: stars, + u_additional22: stars, + u_additional23: stars, + u_additional24: stars, + u_additional25: stars, + u_additional26: stars, + u_additional27: stars, + u_additional28: stars, + u_additional29: stars, + u_additional30: stars, + u_additional31: stars, + u_additional32: stars + } + }); + + expect(material).toBeDefined(); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringMatching( + // this was 31 before, but since moving to vite it's 15. Is there a difference in max texture slots from chromium vs old karma browser? + /Max number texture slots (\d+) have been reached for material "test", no more textures will be uploaded due to hardware constraints\./ + ) + ); }); - - expect(material).toBeDefined(); - expect(logger.warn).toHaveBeenCalledWith( - expect.stringMatching( - // this was 31 before, but since moving to vite it's 15. Is there a difference in max texture slots from chromium vs old karma browser? - /Max number texture slots (\d+) have been reached for material "test", no more textures will be uploaded due to hardware constraints\./ - ) - ); }); }); diff --git a/src/spec/vitest/NineSliceSpec.ts b/src/spec/vitest/NineSliceSpec.ts index 324940b4..13e1217a 100644 --- a/src/spec/vitest/NineSliceSpec.ts +++ b/src/spec/vitest/NineSliceSpec.ts @@ -163,48 +163,50 @@ describe('A NineSlice', () => { expect(newSut).toBeInstanceOf(ex.NineSlice); }); - it('can draw a copy of input tile', async () => { - canvasElement.width = 64; - canvasElement.height = 64; - ctx.clear(); - const sut = new ex.NineSlice(testGraphicConfigSmall); - sut.draw(ctx, 0, 0); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsNineSliceSpec/InputTile.png'); - }); - - it('can draw a tiled copy of input tile', async () => { - canvasElement.width = 128; - canvasElement.height = 128; - ctx.clear(); - const sut = new ex.NineSlice(testGraphicConfigTile); - sut.draw(ctx, 0, 0); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsNineSliceSpec/resultImage_128_128_v_tile_h_tile.png'); - }); - - it('can draw a fitted tile copy of input tile', async () => { - canvasElement.width = 128; - canvasElement.height = 128; - ctx.clear(); - const sut = new ex.NineSlice(testGraphicConfigTileFit); - sut.draw(ctx, 0, 0); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsNineSliceSpec/resultImage_128_128_v_tilefit_h_tilefit.png'); - }); - - it('can draw a stretched copy of input tile', async () => { - canvasElement.width = 128; - canvasElement.height = 128; - ctx.clear(); - const sut = new ex.NineSlice(testGraphicConfigStretch); - sut.draw(ctx, 0, 0); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsNineSliceSpec/resultImage_128_128_v_stretch_h_stretch.png'); - }); - - it('can draw the frame of a tile with no center', async () => { - canvasElement.width = 64; - canvasElement.height = 64; - ctx.clear(); - const sut = new ex.NineSlice(testGraphicConfigNoCenter); - sut.draw(ctx, 0, 0); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsNineSliceSpec/resultImage_64_64_noCenter.png'); + describe('@visual', () => { + it('can draw a copy of input tile', async () => { + canvasElement.width = 64; + canvasElement.height = 64; + ctx.clear(); + const sut = new ex.NineSlice(testGraphicConfigSmall); + sut.draw(ctx, 0, 0); + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsNineSliceSpec/InputTile.png'); + }); + + it('can draw a tiled copy of input tile', async () => { + canvasElement.width = 128; + canvasElement.height = 128; + ctx.clear(); + const sut = new ex.NineSlice(testGraphicConfigTile); + sut.draw(ctx, 0, 0); + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsNineSliceSpec/resultImage_128_128_v_tile_h_tile.png'); + }); + + it('can draw a fitted tile copy of input tile', async () => { + canvasElement.width = 128; + canvasElement.height = 128; + ctx.clear(); + const sut = new ex.NineSlice(testGraphicConfigTileFit); + sut.draw(ctx, 0, 0); + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsNineSliceSpec/resultImage_128_128_v_tilefit_h_tilefit.png'); + }); + + it('can draw a stretched copy of input tile', async () => { + canvasElement.width = 128; + canvasElement.height = 128; + ctx.clear(); + const sut = new ex.NineSlice(testGraphicConfigStretch); + sut.draw(ctx, 0, 0); + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsNineSliceSpec/resultImage_128_128_v_stretch_h_stretch.png'); + }); + + it('can draw the frame of a tile with no center', async () => { + canvasElement.width = 64; + canvasElement.height = 64; + ctx.clear(); + const sut = new ex.NineSlice(testGraphicConfigNoCenter); + sut.draw(ctx, 0, 0); + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsNineSliceSpec/resultImage_64_64_noCenter.png'); + }); }); }); diff --git a/src/spec/vitest/OffscreenSystemSpec.ts b/src/spec/vitest/OffscreenSystemSpec.ts index 34226b50..af7ca506 100644 --- a/src/spec/vitest/OffscreenSystemSpec.ts +++ b/src/spec/vitest/OffscreenSystemSpec.ts @@ -1,7 +1,7 @@ import * as ex from '@excalibur'; import { GraphicsComponent, TransformComponent } from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('The OffscreenSystem', () => { let entities: ex.Entity[]; diff --git a/src/spec/vitest/ParallaxSpec.ts b/src/spec/vitest/ParallaxSpec.ts index 5d089067..74cf8986 100644 --- a/src/spec/vitest/ParallaxSpec.ts +++ b/src/spec/vitest/ParallaxSpec.ts @@ -1,6 +1,6 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('A Parallax Component', () => { it('exists', () => { @@ -16,76 +16,78 @@ describe('A Parallax Component', () => { expect(comp.parallaxFactor).toBeVector(ex.vec(0.5, 0.5)); }); - it('will apply a parallax effect to the graphics', async () => { - const game = TestUtils.engine({ width: 500, height: 500 }, ['use-canvas-context']); - await TestUtils.runToReady(game); - game.currentScene.camera.pos = ex.vec(0, 0); - game.currentScene.camera.drawPos = ex.vec(0, 0); - - const clock = game.clock as ex.TestClock; - - const actor = new ex.Actor({ x: 0, y: 0, width: 120, height: 120, color: ex.Color.Green }); - actor.addComponent(new ex.ParallaxComponent(ex.vec(0.5, 0.5))); - game.add(actor); - - game.currentScene.camera.pos = ex.vec(100, 100); - game.currentScene.camera.drawPos = ex.vec(100, 100); - expect(game.currentScene.camera.pos).toBeVector(ex.vec(100, 100)); - - clock.step(16); - await expect(game.canvas).toEqualImage('/src/spec/assets/images/ParallaxSpec/parallax1.png'); - expect(actor.hasTag('ex.offscreen')).toBe(false); - - game.currentScene.camera.pos = ex.vec(520, 520); - game.currentScene.camera.drawPos = ex.vec(520, 520); - clock.step(); - expect(game.currentScene.camera.pos).toBeVector(ex.vec(520, 520)); - await expect(game.canvas).toEqualImage('/src/spec/assets/images/ParallaxSpec/parallax2.png'); - expect(actor.hasTag('ex.offscreen')).toBe(false); - - game.currentScene.camera.pos = ex.vec(620, 620); - game.currentScene.camera.drawPos = ex.vec(620, 620); - clock.step(); - expect(game.currentScene.camera.pos).toBeVector(ex.vec(620, 620)); - expect(actor.hasTag('ex.offscreen')).toBe(true); - game.dispose(); - }); - - it('works with TileMaps correctly', async () => { - const game = TestUtils.engine({ width: 500, height: 500 }); - await TestUtils.runToReady(game); - const clock = game.clock as ex.TestClock; - - const graphic = new ex.Rectangle({ - color: ex.Color.Green, - strokeColor: ex.Color.Black, - width: 64, - height: 64 - }); - const tilemap = new ex.TileMap({ - tileWidth: 64, - tileHeight: 64, - rows: 4, - columns: 4 + describe('@visual', () => { + it('will apply a parallax effect to the graphics', async () => { + const game = TestUtils.engine({ width: 500, height: 500 }, ['use-canvas-context']); + await TestUtils.runToReady(game); + game.currentScene.camera.pos = ex.vec(0, 0); + game.currentScene.camera.drawPos = ex.vec(0, 0); + + const clock = game.clock as ex.TestClock; + + const actor = new ex.Actor({ x: 0, y: 0, width: 120, height: 120, color: ex.Color.Green }); + actor.addComponent(new ex.ParallaxComponent(ex.vec(0.5, 0.5))); + game.add(actor); + + game.currentScene.camera.pos = ex.vec(100, 100); + game.currentScene.camera.drawPos = ex.vec(100, 100); + expect(game.currentScene.camera.pos).toBeVector(ex.vec(100, 100)); + + clock.step(16); + await expect(game.canvas).toEqualImage('/src/spec/assets/images/ParallaxSpec/parallax1.png'); + expect(actor.hasTag('ex.offscreen')).toBe(false); + + game.currentScene.camera.pos = ex.vec(520, 520); + game.currentScene.camera.drawPos = ex.vec(520, 520); + clock.step(); + expect(game.currentScene.camera.pos).toBeVector(ex.vec(520, 520)); + await expect(game.canvas).toEqualImage('/src/spec/assets/images/ParallaxSpec/parallax2.png'); + expect(actor.hasTag('ex.offscreen')).toBe(false); + + game.currentScene.camera.pos = ex.vec(620, 620); + game.currentScene.camera.drawPos = ex.vec(620, 620); + clock.step(); + expect(game.currentScene.camera.pos).toBeVector(ex.vec(620, 620)); + expect(actor.hasTag('ex.offscreen')).toBe(true); + game.dispose(); }); - tilemap.tiles.forEach((t) => { - t.addGraphic(graphic); - }); - tilemap.addComponent(new ex.ParallaxComponent(ex.vec(0.5, 0.5))); - - game.add(tilemap); - tilemap._initialize(game); - - clock.step(16); - - await expect(game.canvas).toEqualImage('/src/spec/assets/images/ParallaxSpec/tilemap.png'); - game.currentScene.camera.pos = ex.vec(250, -480); - - clock.step(16); // seems like there is an out of phase issue - clock.step(16); - - await expect(game.canvas).toEqualImage('/src/spec/assets/images/ParallaxSpec/tilemap2.png'); - game.dispose(); + it('works with TileMaps correctly', async () => { + const game = TestUtils.engine({ width: 500, height: 500 }); + await TestUtils.runToReady(game); + const clock = game.clock as ex.TestClock; + + const graphic = new ex.Rectangle({ + color: ex.Color.Green, + strokeColor: ex.Color.Black, + width: 64, + height: 64 + }); + const tilemap = new ex.TileMap({ + tileWidth: 64, + tileHeight: 64, + rows: 4, + columns: 4 + }); + tilemap.tiles.forEach((t) => { + t.addGraphic(graphic); + }); + tilemap.addComponent(new ex.ParallaxComponent(ex.vec(0.5, 0.5))); + + game.add(tilemap); + tilemap._initialize(game); + + clock.step(16); + + await expect(game.canvas).toEqualImage('/src/spec/assets/images/ParallaxSpec/tilemap.png'); + + game.currentScene.camera.pos = ex.vec(250, -480); + + clock.step(16); // seems like there is an out of phase issue + clock.step(16); + + await expect(game.canvas).toEqualImage('/src/spec/assets/images/ParallaxSpec/tilemap2.png'); + game.dispose(); + }); }); }); diff --git a/src/spec/vitest/ParticleSpec.ts b/src/spec/vitest/ParticleSpec.ts index dfbde073..e2402cf6 100644 --- a/src/spec/vitest/ParticleSpec.ts +++ b/src/spec/vitest/ParticleSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('A particle', () => { let engine: ex.Engine; @@ -86,139 +86,141 @@ describe('A particle', () => { expect(emitter.random.seed).toBe(1337); }); - it('should emit particles', async () => { - const emitter = new ex.ParticleEmitter({ - pos: new ex.Vector(400, 100), - width: 20, - height: 30, - isEmitting: true, - emitRate: 5, - particle: { - minSpeed: 100, - maxSpeed: 200, - acc: ex.Vector.Zero.clone(), - minAngle: 0, - maxAngle: Math.PI / 2, - life: 4000, - opacity: 0.5, - fade: false, - startSize: 30, - endSize: 40, - beginColor: ex.Color.Red.clone(), - endColor: ex.Color.Blue.clone(), - graphic: null, - angularVelocity: 3, - randomRotation: false - }, - focus: null, - focusAccel: null, - emitterType: ex.EmitterType.Circle, - radius: 20, - random: new ex.Random(1337) - }); - engine.backgroundColor = ex.Color.Transparent; - engine.add(emitter); - emitter.emitParticles(10); + describe('@visual', () => { + it('should emit particles', async () => { + const emitter = new ex.ParticleEmitter({ + pos: new ex.Vector(400, 100), + width: 20, + height: 30, + isEmitting: true, + emitRate: 5, + particle: { + minSpeed: 100, + maxSpeed: 200, + acc: ex.Vector.Zero.clone(), + minAngle: 0, + maxAngle: Math.PI / 2, + life: 4000, + opacity: 0.5, + fade: false, + startSize: 30, + endSize: 40, + beginColor: ex.Color.Red.clone(), + endColor: ex.Color.Blue.clone(), + graphic: null, + angularVelocity: 3, + randomRotation: false + }, + focus: null, + focusAccel: null, + emitterType: ex.EmitterType.Circle, + radius: 20, + random: new ex.Random(1337) + }); + engine.backgroundColor = ex.Color.Transparent; + engine.add(emitter); + emitter.emitParticles(10); - engine.currentScene.update(engine, 100); - engine.currentScene.update(engine, 100); - engine.currentScene.update(engine, 100); - engine.currentScene.draw(engine.graphicsContext, 100); - engine.graphicsContext.flush(); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/ParticleSpec/Particles.png'); - }); - - it('should clear particles', async () => { - const emitter = new ex.ParticleEmitter({ - pos: new ex.Vector(400, 100), - width: 20, - height: 30, - isEmitting: true, - emitRate: 5, - particle: { - minSpeed: 100, - maxSpeed: 200, - acc: ex.Vector.Zero.clone(), - minAngle: 0, - maxAngle: Math.PI / 2, - life: 4000, - opacity: 0.5, - fade: false, - startSize: 30, - endSize: 40, - beginColor: ex.Color.Red.clone(), - endColor: ex.Color.Blue.clone(), - graphic: null, - angularVelocity: 3, - randomRotation: false - }, - focus: null, - focusAccel: null, - emitterType: ex.EmitterType.Circle, - radius: 20, - random: new ex.Random(1337) + engine.currentScene.update(engine, 100); + engine.currentScene.update(engine, 100); + engine.currentScene.update(engine, 100); + engine.currentScene.draw(engine.graphicsContext, 100); + engine.graphicsContext.flush(); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/ParticleSpec/Particles.png'); }); - engine.backgroundColor = ex.Color.Transparent; - engine.add(emitter); - emitter.emitParticles(10); - emitter.clearParticles(); - engine.currentScene.update(engine, 100); - engine.currentScene.update(engine, 100); - engine.currentScene.update(engine, 100); - engine.currentScene.draw(engine.graphicsContext, 100); - engine.graphicsContext.flush(); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/ParticleSpec/clear.png'); - }); + it('should clear particles', async () => { + const emitter = new ex.ParticleEmitter({ + pos: new ex.Vector(400, 100), + width: 20, + height: 30, + isEmitting: true, + emitRate: 5, + particle: { + minSpeed: 100, + maxSpeed: 200, + acc: ex.Vector.Zero.clone(), + minAngle: 0, + maxAngle: Math.PI / 2, + life: 4000, + opacity: 0.5, + fade: false, + startSize: 30, + endSize: 40, + beginColor: ex.Color.Red.clone(), + endColor: ex.Color.Blue.clone(), + graphic: null, + angularVelocity: 3, + randomRotation: false + }, + focus: null, + focusAccel: null, + emitterType: ex.EmitterType.Circle, + radius: 20, + random: new ex.Random(1337) + }); + engine.backgroundColor = ex.Color.Transparent; + engine.add(emitter); + emitter.emitParticles(10); + emitter.clearParticles(); - it('can be parented', async () => { - const emitter = new ex.ParticleEmitter({ - pos: new ex.Vector(0, 0), - width: 20, - height: 30, - isEmitting: true, - emitRate: 5, - particle: { - minSpeed: 100, - maxSpeed: 200, - acc: ex.Vector.Zero.clone(), - minAngle: 0, - maxAngle: Math.PI / 2, - life: 4000, - opacity: 0.5, - fade: false, - startSize: 30, - endSize: 40, - beginColor: ex.Color.Red.clone(), - endColor: ex.Color.Blue.clone(), - graphic: null, - angularVelocity: 3, - randomRotation: false - }, - focus: null, - focusAccel: null, - emitterType: ex.EmitterType.Circle, - radius: 20, - random: new ex.Random(1337) + engine.currentScene.update(engine, 100); + engine.currentScene.update(engine, 100); + engine.currentScene.update(engine, 100); + engine.currentScene.draw(engine.graphicsContext, 100); + engine.graphicsContext.flush(); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/ParticleSpec/clear.png'); }); - const parent = new ex.Actor({ - pos: ex.vec(100, 50), - width: 10, - height: 10 - }); - parent.addChild(emitter); + it('can be parented', async () => { + const emitter = new ex.ParticleEmitter({ + pos: new ex.Vector(0, 0), + width: 20, + height: 30, + isEmitting: true, + emitRate: 5, + particle: { + minSpeed: 100, + maxSpeed: 200, + acc: ex.Vector.Zero.clone(), + minAngle: 0, + maxAngle: Math.PI / 2, + life: 4000, + opacity: 0.5, + fade: false, + startSize: 30, + endSize: 40, + beginColor: ex.Color.Red.clone(), + endColor: ex.Color.Blue.clone(), + graphic: null, + angularVelocity: 3, + randomRotation: false + }, + focus: null, + focusAccel: null, + emitterType: ex.EmitterType.Circle, + radius: 20, + random: new ex.Random(1337) + }); - engine.backgroundColor = ex.Color.Transparent; - engine.add(emitter); + const parent = new ex.Actor({ + pos: ex.vec(100, 50), + width: 10, + height: 10 + }); + parent.addChild(emitter); - emitter.emitParticles(20); - engine.currentScene.update(engine, 100); - engine.currentScene.update(engine, 100); - engine.currentScene.update(engine, 100); - engine.currentScene.draw(engine.graphicsContext, 100); - engine.graphicsContext.flush(); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/ParticleSpec/parented.png'); + engine.backgroundColor = ex.Color.Transparent; + engine.add(emitter); + + emitter.emitParticles(20); + engine.currentScene.update(engine, 100); + engine.currentScene.update(engine, 100); + engine.currentScene.update(engine, 100); + engine.currentScene.draw(engine.graphicsContext, 100); + engine.graphicsContext.flush(); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/ParticleSpec/parented.png'); + }); }); it('can set the particle transform to local making particles children of the emitter', () => { diff --git a/src/spec/vitest/PhysicsWorldSpec.ts b/src/spec/vitest/PhysicsWorldSpec.ts index c8bed71a..0843d8ce 100644 --- a/src/spec/vitest/PhysicsWorldSpec.ts +++ b/src/spec/vitest/PhysicsWorldSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('A physics world', () => { beforeEach(() => { diff --git a/src/spec/vitest/PointerInputSpec.ts b/src/spec/vitest/PointerInputSpec.ts index 0deceaa3..40f87767 100644 --- a/src/spec/vitest/PointerInputSpec.ts +++ b/src/spec/vitest/PointerInputSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('A pointer', () => { let engine: ex.Engine = null; diff --git a/src/spec/vitest/PolygonSpec.ts b/src/spec/vitest/PolygonSpec.ts index e0762391..3d9bd943 100644 --- a/src/spec/vitest/PolygonSpec.ts +++ b/src/spec/vitest/PolygonSpec.ts @@ -44,15 +44,17 @@ describe('A Polygon Graphic', () => { expect(clone.strokeColor).toEqual(poly.strokeColor); }); - it('can be drawn', async () => { - const poly = new ex.Polygon({ - points: [ex.vec(10 * 5, 0), ex.vec(0, 20 * 5), ex.vec(20 * 5, 20 * 5)], - color: ex.Color.Green, - strokeColor: ex.Color.Violet - }); + describe('@visual', () => { + it('can be drawn', async () => { + const poly = new ex.Polygon({ + points: [ex.vec(10 * 5, 0), ex.vec(0, 20 * 5), ex.vec(20 * 5, 20 * 5)], + color: ex.Color.Green, + strokeColor: ex.Color.Violet + }); - poly.draw(ctx, 0, 0); + poly.draw(ctx, 0, 0); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsPolygonSpec/poly.png', 0.993); + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsPolygonSpec/poly.png', 0.993); + }); }); }); diff --git a/src/spec/ProjectionsSpec.ts b/src/spec/vitest/ProjectionsSpec.ts similarity index 96% rename from src/spec/ProjectionsSpec.ts rename to src/spec/vitest/ProjectionsSpec.ts index 5a5661d9..4052e901 100644 --- a/src/spec/ProjectionsSpec.ts +++ b/src/spec/vitest/ProjectionsSpec.ts @@ -1,4 +1,4 @@ -import * as ex from 'excalibur'; +import * as ex from '@excalibur'; describe('Projections', () => { it('exists', () => { diff --git a/src/spec/vitest/RasterSpec.ts b/src/spec/vitest/RasterSpec.ts index 52ef8ad5..725de80c 100644 --- a/src/spec/vitest/RasterSpec.ts +++ b/src/spec/vitest/RasterSpec.ts @@ -109,14 +109,25 @@ describe('A Raster', () => { expect(sut.cloneRasterOptions()).toEqual(originalRasterOptions); }); - it('can be drawn', async () => { - const sut = new TestRaster(); - vi.spyOn(sut, 'rasterize'); - vi.spyOn(sut, 'execute'); - sut.draw(ctx, 0, 0); - expect(sut.execute).toHaveBeenCalledTimes(1); + describe('@visual', () => { + it('can be drawn', async () => { + const sut = new TestRaster(); + vi.spyOn(sut, 'rasterize'); + vi.spyOn(sut, 'execute'); + sut.draw(ctx, 0, 0); + expect(sut.execute).toHaveBeenCalledTimes(1); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsRasterSpec/raster.png'); + }); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsRasterSpec/raster.png'); + it('can have the quality increased', async () => { + const sut = new TestRaster({ + quality: 4 + }); + sut.quality = 4; + sut.draw(ctx, 25, 25); + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsRasterSpec/raster-quality.png'); + }); }); it('knows when it must be re-rastered', () => { @@ -128,15 +139,6 @@ describe('A Raster', () => { expect(sut.dirty).toBe(true); }); - it('can have the quality increased', async () => { - const sut = new TestRaster({ - quality: 4 - }); - sut.quality = 4; - sut.draw(ctx, 25, 25); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsRasterSpec/raster-quality.png'); - }); - it('can have padding and maintain correct size and not grow after each draw', () => { const sut = new TestRaster(); expect(sut.localBounds.width).toBe(50); diff --git a/src/spec/vitest/RectangleSpec.ts b/src/spec/vitest/RectangleSpec.ts index 479236cb..1ff84ac0 100644 --- a/src/spec/vitest/RectangleSpec.ts +++ b/src/spec/vitest/RectangleSpec.ts @@ -22,38 +22,40 @@ describe('A Rectangle Graphic', () => { expect(rect).toBeDefined(); }); - it('can be drawn', async () => { - const rect = new ex.Rectangle({ - width: 50, - height: 50, - color: ex.Color.Green - }); + describe('@visual', () => { + it('can be drawn', async () => { + const rect = new ex.Rectangle({ + width: 50, + height: 50, + color: ex.Color.Green + }); - rect.draw(ctx, 25, 25); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsRectSpec/rect.png'); - }); + rect.draw(ctx, 25, 25); - it('can be cloned', async () => { - const rect = new ex.Rectangle({ - width: 75, - height: 50, - lineWidth: 10, - lineDash: [5, 5], - color: ex.Color.Green, - strokeColor: ex.Color.Violet + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsRectSpec/rect.png'); }); - const rect2 = rect.clone(); - expect(rect2.width).toBe(75); - expect(rect2.height).toBe(50); - expect(rect2.lineDash).toEqual([5, 5]); - expect(rect2.lineWidth).toBe(10); - expect(rect2.color).toEqual(ex.Color.Green); - expect(rect2.strokeColor).toEqual(ex.Color.Violet); - - rect2.draw(ctx, 15, 25); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsRectSpec/clone-rect.png'); + it('can be cloned', async () => { + const rect = new ex.Rectangle({ + width: 75, + height: 50, + lineWidth: 10, + lineDash: [5, 5], + color: ex.Color.Green, + strokeColor: ex.Color.Violet + }); + + const rect2 = rect.clone(); + expect(rect2.width).toBe(75); + expect(rect2.height).toBe(50); + expect(rect2.lineDash).toEqual([5, 5]); + expect(rect2.lineWidth).toBe(10); + expect(rect2.color).toEqual(ex.Color.Green); + expect(rect2.strokeColor).toEqual(ex.Color.Violet); + + rect2.draw(ctx, 15, 25); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsRectSpec/clone-rect.png'); + }); }); }); diff --git a/src/spec/vitest/ResourceSpec.ts b/src/spec/vitest/ResourceSpec.ts index 3a475c02..7d721165 100644 --- a/src/spec/vitest/ResourceSpec.ts +++ b/src/spec/vitest/ResourceSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('A generic Resource', () => { let resource: ex.Resource; diff --git a/src/spec/vitest/ScaleSpec.ts b/src/spec/vitest/ScaleSpec.ts index 1ad68e36..1f2941bf 100644 --- a/src/spec/vitest/ScaleSpec.ts +++ b/src/spec/vitest/ScaleSpec.ts @@ -1,6 +1,6 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; -import { Mocks } from './util/Mocks'; +import { TestUtils } from '../__util__/TestUtils'; +import { Mocks } from '../__util__/Mocks'; describe('A scaled and rotated actor', () => { let actor: ex.Actor; @@ -21,34 +21,35 @@ describe('A scaled and rotated actor', () => { engine.stop(); engine.dispose(); }); + describe('@visual', () => { + it('is drawn correctly scaled at 90 degrees', () => + new Promise((done) => { + const clock = engine.clock as ex.TestClock; + const bg = new ex.ImageSource('/src/spec/assets/images/ScaleSpec/logo.png'); + const loader = new ex.Loader([bg]); + TestUtils.runToReady(engine, loader).then(() => { + const actor = new ex.Actor({ + x: engine.halfDrawWidth, + y: engine.halfDrawHeight, + width: 100, + height: 10, + color: ex.Color.Black + }); + actor.graphics.use(bg.toSprite()); + actor.scale.setTo(1, 0.2); + engine.add(actor); - it('is drawn correctly scaled at 90 degrees', () => - new Promise((done) => { - const clock = engine.clock as ex.TestClock; - const bg = new ex.ImageSource('/src/spec/assets/images/ScaleSpec/logo.png'); - const loader = new ex.Loader([bg]); - TestUtils.runToReady(engine, loader).then(() => { - const actor = new ex.Actor({ - x: engine.halfDrawWidth, - y: engine.halfDrawHeight, - width: 100, - height: 10, - color: ex.Color.Black - }); - actor.graphics.use(bg.toSprite()); - actor.scale.setTo(1, 0.2); - engine.add(actor); - - actor.rotation = Math.PI / 2; + actor.rotation = Math.PI / 2; - clock.step(1); + clock.step(1); - expect(engine.canvas) - .toEqualImage('/src/spec/assets/images/ScaleSpec/scale.png') - .then(() => { - done(); - engine.dispose(); - }); - }); - })); + expect(engine.canvas) + .toEqualImage('/src/spec/assets/images/ScaleSpec/scale.png') + .then(() => { + done(); + engine.dispose(); + }); + }); + })); + }); }); diff --git a/src/spec/vitest/SceneSpec.ts b/src/spec/vitest/SceneSpec.ts index fbb99b06..58dfaa36 100644 --- a/src/spec/vitest/SceneSpec.ts +++ b/src/spec/vitest/SceneSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('A scene', () => { let actor: ex.Actor; diff --git a/src/spec/vitest/ScreenAppenderSpec.ts b/src/spec/vitest/ScreenAppenderSpec.ts index fecbf484..e5ecd23e 100644 --- a/src/spec/vitest/ScreenAppenderSpec.ts +++ b/src/spec/vitest/ScreenAppenderSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('A ScreenAppender', () => { it('exists', () => { @@ -13,29 +13,31 @@ describe('A ScreenAppender', () => { engine.dispose(); }); - it('can be configured to log', async () => { - const engine = TestUtils.engine({ width: 100, height: 100 }); - const sut = new ex.ScreenAppender({ - engine, - color: ex.Color.Yellow, - xPos: 0 - }); + describe('@visual', () => { + it('can be configured to log', async () => { + const engine = TestUtils.engine({ width: 100, height: 100 }); + const sut = new ex.ScreenAppender({ + engine, + color: ex.Color.Yellow, + xPos: 0 + }); - sut.log(ex.LogLevel.Info, ['this is a log']); - sut.log(ex.LogLevel.Info, ['this is a log']); - sut.log(ex.LogLevel.Info, ['this is a log']); - sut.log(ex.LogLevel.Info, ['this is a log']); - sut.log(ex.LogLevel.Info, ['this is a log']); - sut.log(ex.LogLevel.Info, ['this is a log']); + sut.log(ex.LogLevel.Info, ['this is a log']); + sut.log(ex.LogLevel.Info, ['this is a log']); + sut.log(ex.LogLevel.Info, ['this is a log']); + sut.log(ex.LogLevel.Info, ['this is a log']); + sut.log(ex.LogLevel.Info, ['this is a log']); + sut.log(ex.LogLevel.Info, ['this is a log']); - await TestUtils.runOnWindows(async () => { - await expect(sut.canvas).toEqualImage('/src/spec/assets/images/ScreenAppenderSpec/screen-log.png'); - }); + await TestUtils.runOnWindows(async () => { + await expect(sut.canvas).toEqualImage('/src/spec/assets/images/ScreenAppenderSpec/screen-log.png'); + }); - // FIXME CI chokes on this one for some reason - // await TestUtils.runOnLinux(async () => { - // await expect(sut.canvas).toEqualImage('src/spec/assets/images/ScreenAppenderSpec/screen-log-linux.png'); - // }); - engine.dispose(); + // FIXME CI chokes on this one for some reason + // await TestUtils.runOnLinux(async () => { + // await expect(sut.canvas).toEqualImage('src/spec/assets/images/ScreenAppenderSpec/screen-log-linux.png'); + // }); + engine.dispose(); + }); }); }); diff --git a/src/spec/vitest/ScreenElementSpec.ts b/src/spec/vitest/ScreenElementSpec.ts index 8df59aac..098ee6b3 100644 --- a/src/spec/vitest/ScreenElementSpec.ts +++ b/src/spec/vitest/ScreenElementSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; import { ScreenElement } from '@excalibur'; describe('A ScreenElement', () => { @@ -119,18 +119,20 @@ describe('A ScreenElement', () => { expect(screenElement.contains(50, 50, true)).toBe(true); }); - it('is drawn on the top left with empty constructor', async () => { - const game = TestUtils.engine({ width: 720, height: 480 }); - const clock = game.clock as ex.TestClock; - const bg = new ex.ImageSource('/src/spec/assets/images/ScreenElementSpec/emptyctor.png'); - const loader = new ex.Loader([bg]); - await TestUtils.runToReady(game, loader); - const screenElement = new ex.ScreenElement(); - screenElement.graphics.use(bg.toSprite()); - game.add(screenElement); - game.currentScene.draw(game.graphicsContext, 100); - game.graphicsContext.flush(); - await expect(game.canvas).toEqualImage('/src/spec/assets/images/ScreenElementSpec/emptyctor.png'); - game.dispose(); + describe('@visual', () => { + it('is drawn on the top left with empty constructor', async () => { + const game = TestUtils.engine({ width: 720, height: 480 }); + const clock = game.clock as ex.TestClock; + const bg = new ex.ImageSource('/src/spec/assets/images/ScreenElementSpec/emptyctor.png'); + const loader = new ex.Loader([bg]); + await TestUtils.runToReady(game, loader); + const screenElement = new ex.ScreenElement(); + screenElement.graphics.use(bg.toSprite()); + game.add(screenElement); + game.currentScene.draw(game.graphicsContext, 100); + game.graphicsContext.flush(); + await expect(game.canvas).toEqualImage('/src/spec/assets/images/ScreenElementSpec/emptyctor.png'); + game.dispose(); + }); }); }); diff --git a/src/spec/vitest/SlideSpec.ts b/src/spec/vitest/SlideSpec.ts index 9f6cd47b..1230d49c 100644 --- a/src/spec/vitest/SlideSpec.ts +++ b/src/spec/vitest/SlideSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('A Slide transition', () => { it('exists', () => { @@ -11,203 +11,204 @@ describe('A Slide transition', () => { expect(sut.duration).toBe(1000); expect(sut.name).toContain('Slide#'); }); + describe('@visual', () => { + it('can slide down', async () => { + const engine = TestUtils.engine({ backgroundColor: ex.Color.ExcaliburBlue }); + const clock = engine.clock as ex.TestClock; + await TestUtils.runToReady(engine); + engine.add( + new ex.Actor({ + pos: ex.vec(20, 20), + width: 100, + height: 100, + color: ex.Color.Red + }) + ); + + const onDeactivateSpy = vi.fn(); + + engine.director.getSceneInstance('root').onDeactivate = onDeactivateSpy; + + const sut = new ex.Slide({ duration: 1000, slideDirection: 'down' }); + const scene = new ex.Scene(); + scene.add( + new ex.Actor({ + pos: ex.vec(200, 200), + width: 40, + height: 40, + color: ex.Color.Violet + }) + ); + engine.addScene('newScene', { scene, transitions: { in: sut } }); + + const goto = engine.goToScene('newScene'); + await TestUtils.flushMicrotasks(clock, 15); + await TestUtils.untilMacrotask(() => engine.currentSceneName !== 'root'); + clock.step(500); + expect(onDeactivateSpy).toHaveBeenCalledTimes(1); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/SlideSpec/slide-down.png'); + engine.stop(); + engine.dispose(); + }); - it('can slide down', async () => { - const engine = TestUtils.engine({ backgroundColor: ex.Color.ExcaliburBlue }); - const clock = engine.clock as ex.TestClock; - await TestUtils.runToReady(engine); - engine.add( - new ex.Actor({ - pos: ex.vec(20, 20), - width: 100, - height: 100, - color: ex.Color.Red - }) - ); - - const onDeactivateSpy = vi.fn(); - - engine.director.getSceneInstance('root').onDeactivate = onDeactivateSpy; - - const sut = new ex.Slide({ duration: 1000, slideDirection: 'down' }); - const scene = new ex.Scene(); - scene.add( - new ex.Actor({ - pos: ex.vec(200, 200), - width: 40, - height: 40, - color: ex.Color.Violet - }) - ); - engine.addScene('newScene', { scene, transitions: { in: sut } }); - - const goto = engine.goToScene('newScene'); - await TestUtils.flushMicrotasks(clock, 15); - await TestUtils.untilMacrotask(() => engine.currentSceneName !== 'root'); - clock.step(500); - expect(onDeactivateSpy).toHaveBeenCalledTimes(1); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/SlideSpec/slide-down.png'); - engine.stop(); - engine.dispose(); - }); - - it('can slide right', async () => { - const engine = TestUtils.engine({ backgroundColor: ex.Color.ExcaliburBlue }); - const clock = engine.clock as ex.TestClock; - await TestUtils.runToReady(engine); - engine.add( - new ex.Actor({ - pos: ex.vec(20, 20), - width: 100, - height: 100, - color: ex.Color.Red - }) - ); - - const onDeactivateSpy = vi.fn(); - - engine.director.getSceneInstance('root').onDeactivate = onDeactivateSpy; - - const sut = new ex.Slide({ duration: 1000, slideDirection: 'right' }); - const scene = new ex.Scene(); - scene.add( - new ex.Actor({ - pos: ex.vec(200, 200), - width: 40, - height: 40, - color: ex.Color.Violet - }) - ); - engine.addScene('newScene', { scene, transitions: { in: sut } }); - - const goto = engine.goToScene('newScene'); - await TestUtils.flushMicrotasks(clock, 15); - await TestUtils.untilMacrotask(() => engine.currentSceneName !== 'root'); - clock.step(500); - expect(onDeactivateSpy).toHaveBeenCalledTimes(1); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/SlideSpec/slide-right.png'); - engine.stop(); - engine.dispose(); - }); - - it('can slide left', async () => { - const engine = TestUtils.engine({ backgroundColor: ex.Color.ExcaliburBlue }); - const clock = engine.clock as ex.TestClock; - await TestUtils.runToReady(engine); - engine.add( - new ex.Actor({ - pos: ex.vec(400, 200), - width: 100, - height: 100, - color: ex.Color.Red - }) - ); - - const onDeactivateSpy = vi.fn(); - - engine.director.getSceneInstance('root').onDeactivate = onDeactivateSpy; - - const sut = new ex.Slide({ duration: 1000, slideDirection: 'left' }); - const scene = new ex.Scene(); - scene.add( - new ex.Actor({ - pos: ex.vec(200, 200), - width: 40, - height: 40, - color: ex.Color.Violet - }) - ); - engine.addScene('newScene', { scene, transitions: { in: sut } }); - - const goto = engine.goToScene('newScene'); - await TestUtils.flushMicrotasks(clock, 15); - await TestUtils.untilMacrotask(() => engine.currentSceneName !== 'root'); - clock.step(500); - expect(onDeactivateSpy).toHaveBeenCalledTimes(1); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/SlideSpec/slide-left.png'); - engine.stop(); - engine.dispose(); - }); - - it('can slide up', async () => { - const engine = TestUtils.engine({ backgroundColor: ex.Color.ExcaliburBlue }); - const clock = engine.clock as ex.TestClock; - await TestUtils.runToReady(engine); - engine.add( - new ex.Actor({ - pos: ex.vec(400, 400), - width: 100, - height: 100, - color: ex.Color.Red - }) - ); - - const onDeactivateSpy = vi.fn(); + it('can slide right', async () => { + const engine = TestUtils.engine({ backgroundColor: ex.Color.ExcaliburBlue }); + const clock = engine.clock as ex.TestClock; + await TestUtils.runToReady(engine); + engine.add( + new ex.Actor({ + pos: ex.vec(20, 20), + width: 100, + height: 100, + color: ex.Color.Red + }) + ); + + const onDeactivateSpy = vi.fn(); + + engine.director.getSceneInstance('root').onDeactivate = onDeactivateSpy; + + const sut = new ex.Slide({ duration: 1000, slideDirection: 'right' }); + const scene = new ex.Scene(); + scene.add( + new ex.Actor({ + pos: ex.vec(200, 200), + width: 40, + height: 40, + color: ex.Color.Violet + }) + ); + engine.addScene('newScene', { scene, transitions: { in: sut } }); + + const goto = engine.goToScene('newScene'); + await TestUtils.flushMicrotasks(clock, 15); + await TestUtils.untilMacrotask(() => engine.currentSceneName !== 'root'); + clock.step(500); + expect(onDeactivateSpy).toHaveBeenCalledTimes(1); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/SlideSpec/slide-right.png'); + engine.stop(); + engine.dispose(); + }); - engine.director.getSceneInstance('root').onDeactivate = onDeactivateSpy; + it('can slide left', async () => { + const engine = TestUtils.engine({ backgroundColor: ex.Color.ExcaliburBlue }); + const clock = engine.clock as ex.TestClock; + await TestUtils.runToReady(engine); + engine.add( + new ex.Actor({ + pos: ex.vec(400, 200), + width: 100, + height: 100, + color: ex.Color.Red + }) + ); + + const onDeactivateSpy = vi.fn(); + + engine.director.getSceneInstance('root').onDeactivate = onDeactivateSpy; + + const sut = new ex.Slide({ duration: 1000, slideDirection: 'left' }); + const scene = new ex.Scene(); + scene.add( + new ex.Actor({ + pos: ex.vec(200, 200), + width: 40, + height: 40, + color: ex.Color.Violet + }) + ); + engine.addScene('newScene', { scene, transitions: { in: sut } }); + + const goto = engine.goToScene('newScene'); + await TestUtils.flushMicrotasks(clock, 15); + await TestUtils.untilMacrotask(() => engine.currentSceneName !== 'root'); + clock.step(500); + expect(onDeactivateSpy).toHaveBeenCalledTimes(1); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/SlideSpec/slide-left.png'); + engine.stop(); + engine.dispose(); + }); - const sut = new ex.Slide({ duration: 1000, slideDirection: 'up' }); - const scene = new ex.Scene(); - scene.add( - new ex.Actor({ - pos: ex.vec(200, 200), - width: 40, - height: 40, - color: ex.Color.Violet - }) - ); - engine.addScene('newScene', { scene, transitions: { in: sut } }); - - const goto = engine.goToScene('newScene'); - await TestUtils.flushMicrotasks(clock, 15); - await TestUtils.untilMacrotask(() => engine.currentSceneName !== 'root'); - clock.step(500); - expect(onDeactivateSpy).toHaveBeenCalledTimes(1); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/SlideSpec/slide-up.png'); - engine.stop(); - engine.dispose(); - }); + it('can slide up', async () => { + const engine = TestUtils.engine({ backgroundColor: ex.Color.ExcaliburBlue }); + const clock = engine.clock as ex.TestClock; + await TestUtils.runToReady(engine); + engine.add( + new ex.Actor({ + pos: ex.vec(400, 400), + width: 100, + height: 100, + color: ex.Color.Red + }) + ); + + const onDeactivateSpy = vi.fn(); + + engine.director.getSceneInstance('root').onDeactivate = onDeactivateSpy; + + const sut = new ex.Slide({ duration: 1000, slideDirection: 'up' }); + const scene = new ex.Scene(); + scene.add( + new ex.Actor({ + pos: ex.vec(200, 200), + width: 40, + height: 40, + color: ex.Color.Violet + }) + ); + engine.addScene('newScene', { scene, transitions: { in: sut } }); + + const goto = engine.goToScene('newScene'); + await TestUtils.flushMicrotasks(clock, 15); + await TestUtils.untilMacrotask(() => engine.currentSceneName !== 'root'); + clock.step(500); + expect(onDeactivateSpy).toHaveBeenCalledTimes(1); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/SlideSpec/slide-up.png'); + engine.stop(); + engine.dispose(); + }); - it('can slide up with custom easing', async () => { - const engine = TestUtils.engine({ backgroundColor: ex.Color.ExcaliburBlue }); - const clock = engine.clock as ex.TestClock; - await TestUtils.runToReady(engine); - engine.add( - new ex.Actor({ - pos: ex.vec(450, 450), - width: 100, - height: 100, - color: ex.Color.Red - }) - ); - - const onDeactivateSpy = vi.fn(); - - engine.director.getSceneInstance('root').onDeactivate = onDeactivateSpy; - - const sut = new ex.Slide({ - duration: 1000, - slideDirection: 'up', - easingFunction: ex.EasingFunctions.EaseInOutCubic + it('can slide up with custom easing', async () => { + const engine = TestUtils.engine({ backgroundColor: ex.Color.ExcaliburBlue }); + const clock = engine.clock as ex.TestClock; + await TestUtils.runToReady(engine); + engine.add( + new ex.Actor({ + pos: ex.vec(450, 450), + width: 100, + height: 100, + color: ex.Color.Red + }) + ); + + const onDeactivateSpy = vi.fn(); + + engine.director.getSceneInstance('root').onDeactivate = onDeactivateSpy; + + const sut = new ex.Slide({ + duration: 1000, + slideDirection: 'up', + easingFunction: ex.EasingFunctions.EaseInOutCubic + }); + const scene = new ex.Scene(); + scene.add( + new ex.Actor({ + pos: ex.vec(200, 200), + width: 40, + height: 40, + color: ex.Color.Violet + }) + ); + engine.addScene('newScene', { scene, transitions: { in: sut } }); + + const goto = engine.goToScene('newScene'); + await TestUtils.flushMicrotasks(clock, 15); + await TestUtils.untilMacrotask(() => engine.currentSceneName !== 'root'); + clock.step(700); + expect(onDeactivateSpy).toHaveBeenCalledTimes(1); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/SlideSpec/slide-up-transition.png'); + engine.stop(); + engine.dispose(); }); - const scene = new ex.Scene(); - scene.add( - new ex.Actor({ - pos: ex.vec(200, 200), - width: 40, - height: 40, - color: ex.Color.Violet - }) - ); - engine.addScene('newScene', { scene, transitions: { in: sut } }); - - const goto = engine.goToScene('newScene'); - await TestUtils.flushMicrotasks(clock, 15); - await TestUtils.untilMacrotask(() => engine.currentSceneName !== 'root'); - clock.step(700); - expect(onDeactivateSpy).toHaveBeenCalledTimes(1); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/SlideSpec/slide-up-transition.png'); - engine.stop(); - engine.dispose(); }); }); diff --git a/src/spec/vitest/SoundSpec.ts b/src/spec/vitest/SoundSpec.ts index a323ed2e..7fbe1579 100644 --- a/src/spec/vitest/SoundSpec.ts +++ b/src/spec/vitest/SoundSpec.ts @@ -2,7 +2,7 @@ import * as ex from '@excalibur'; import { canPlayFile } from '../../engine/Util/Sound'; import { delay } from '../../engine/Util/Util'; import { WebAudio } from '../../engine/Util/WebAudio'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; import { page } from '@vitest/browser/context'; diff --git a/src/spec/vitest/SpriteSheetSpec.ts b/src/spec/vitest/SpriteSheetSpec.ts index 714bbafd..17e7ab8a 100644 --- a/src/spec/vitest/SpriteSheetSpec.ts +++ b/src/spec/vitest/SpriteSheetSpec.ts @@ -65,66 +65,68 @@ describe('A SpriteSheet for Graphics', () => { expect(ss.sprites[0].height).toBe(16); }); - it('can be created from a grid with interesting spacing', async () => { - const image = new ex.ImageSource('/src/spec/assets/images/SpriteSheetSpec/kenny-cards.png'); - - await image.load(); - - const ss = ex.SpriteSheet.fromImageSource({ - image, - grid: { - rows: 4, - columns: 14, - spriteWidth: 42, - spriteHeight: 60 - }, - spacing: { - originOffset: { x: 11, y: 2 }, - margin: { x: 23, y: 5 } - } + describe('@visual', () => { + it('can be created from a grid with interesting spacing', async () => { + const image = new ex.ImageSource('/src/spec/assets/images/SpriteSheetSpec/kenny-cards.png'); + + await image.load(); + + const ss = ex.SpriteSheet.fromImageSource({ + image, + grid: { + rows: 4, + columns: 14, + spriteWidth: 42, + spriteHeight: 60 + }, + spacing: { + originOffset: { x: 11, y: 2 }, + margin: { x: 23, y: 5 } + } + }); + + ctx.clear(); + + ss.sprites[12].draw(ctx, 0, 0); + ss.sprites[24].draw(ctx, 60, 0); + ss.sprites[36].draw(ctx, 0, 60); + ss.sprites[48].draw(ctx, 60, 60); + + expect(ss.sprites.length).toBe(4 * 14); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/SpriteSheetSpec/NewSpriteSpacing.png'); }); - ctx.clear(); + it('can be created from with spacing using vectors', async () => { + const image = new ex.ImageSource('/src/spec/assets/images/SpriteSheetSpec/kenny-cards.png'); - ss.sprites[12].draw(ctx, 0, 0); - ss.sprites[24].draw(ctx, 60, 0); - ss.sprites[36].draw(ctx, 0, 60); - ss.sprites[48].draw(ctx, 60, 60); + await image.load(); - expect(ss.sprites.length).toBe(4 * 14); + const ss = ex.SpriteSheet.fromImageSource({ + image, + grid: { + rows: 4, + columns: 14, + spriteWidth: 42, + spriteHeight: 60 + }, + spacing: { + originOffset: ex.vec(11, 2), + margin: ex.vec(23, 5) + } + }); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/SpriteSheetSpec/NewSpriteSpacing.png'); - }); + ctx.clear(); - it('can be created from with spacing using vectors', async () => { - const image = new ex.ImageSource('/src/spec/assets/images/SpriteSheetSpec/kenny-cards.png'); + ss.sprites[12].draw(ctx, 0, 0); + ss.sprites[24].draw(ctx, 60, 0); + ss.sprites[36].draw(ctx, 0, 60); + ss.sprites[48].draw(ctx, 60, 60); - await image.load(); + expect(ss.sprites.length).toBe(4 * 14); - const ss = ex.SpriteSheet.fromImageSource({ - image, - grid: { - rows: 4, - columns: 14, - spriteWidth: 42, - spriteHeight: 60 - }, - spacing: { - originOffset: ex.vec(11, 2), - margin: ex.vec(23, 5) - } + await expect(canvasElement).toEqualImage('/src/spec/assets/images/SpriteSheetSpec/NewSpriteSpacing.png'); }); - - ctx.clear(); - - ss.sprites[12].draw(ctx, 0, 0); - ss.sprites[24].draw(ctx, 60, 0); - ss.sprites[36].draw(ctx, 0, 60); - ss.sprites[48].draw(ctx, 60, 60); - - expect(ss.sprites.length).toBe(4 * 14); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/SpriteSheetSpec/NewSpriteSpacing.png'); }); it('can retrieve a sprite by x,y', async () => { diff --git a/src/spec/vitest/SpriteSpec.ts b/src/spec/vitest/SpriteSpec.ts index 7546cb59..9f52595d 100644 --- a/src/spec/vitest/SpriteSpec.ts +++ b/src/spec/vitest/SpriteSpec.ts @@ -106,205 +106,207 @@ describe('A Sprite Graphic', () => { expect(sut.destSize.height).toBe(55); }); - it('can specify a source/dest view of an image with default width and height', async () => { - const image = new ex.ImageSource('/src/spec/assets/images/GraphicsTextSpec/spritefont.png'); - const sut = new ex.Sprite({ - image, - width: 16, - height: 16 - }); - - expect(sut.width, 'Graphic width should be 16').toBe(16); - expect(sut.height, 'Graphic height should be 16').toBe(16); - expect(sut.sourceView.x).toBe(0); - expect(sut.sourceView.y).toBe(0); - expect(sut.sourceView.width, 'Graphic sourceView width should be 16').toBe(16); - expect(sut.sourceView.height, 'Graphic sourceView height should be 16').toBe(16); - expect(sut.destSize.width, 'Graphic destSize width should be 16').toBe(16); - expect(sut.destSize.height, 'Graphic destSize height should be 16').toBe(16); - expect(sut.localBounds.width, 'Graphic local bounds width should be 16').toBe(16); - expect(sut.localBounds.height, 'Graphic local bounds height should be 16').toBe(16); - - await image.load(); - await image.ready; - - ctx.clear(); - sut.draw(ctx, 50 - sut.width / 2, 50 - sut.width / 2); - ctx.flush(); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsSpriteSpec/source-view.png'); - }); - - it('can draw an sprite image with a tint', async () => { - const image = new ex.ImageSource('/src/spec/assets/images/GraphicsSpriteSpec/icon.png'); - const sut = image.toSprite(); - sut.tint = ex.Color.Green; - - await image.load(); - await image.ready; - - ctx.clear(); - sut.draw(ctx, 0, 0); - ctx.flush(); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsSpriteSpec/icon-tint.png'); - }); - - it('can specify the width and height of a sprite after construction', async () => { - const image = new ex.ImageSource('/src/spec/assets/images/GraphicsTextSpec/spritefont.png', false, ex.ImageFiltering.Pixel); - const sut = new ex.Sprite({ - image, - sourceView: { - x: 0, - y: 0, + describe('@visual', () => { + it('can specify a source/dest view of an image with default width and height', async () => { + const image = new ex.ImageSource('/src/spec/assets/images/GraphicsTextSpec/spritefont.png'); + const sut = new ex.Sprite({ + image, width: 16, height: 16 - } + }); + + expect(sut.width, 'Graphic width should be 16').toBe(16); + expect(sut.height, 'Graphic height should be 16').toBe(16); + expect(sut.sourceView.x).toBe(0); + expect(sut.sourceView.y).toBe(0); + expect(sut.sourceView.width, 'Graphic sourceView width should be 16').toBe(16); + expect(sut.sourceView.height, 'Graphic sourceView height should be 16').toBe(16); + expect(sut.destSize.width, 'Graphic destSize width should be 16').toBe(16); + expect(sut.destSize.height, 'Graphic destSize height should be 16').toBe(16); + expect(sut.localBounds.width, 'Graphic local bounds width should be 16').toBe(16); + expect(sut.localBounds.height, 'Graphic local bounds height should be 16').toBe(16); + + await image.load(); + await image.ready; + + ctx.clear(); + sut.draw(ctx, 50 - sut.width / 2, 50 - sut.width / 2); + ctx.flush(); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsSpriteSpec/source-view.png'); }); - sut.width = 64; - sut.height = 64; + it('can draw an sprite image with a tint', async () => { + const image = new ex.ImageSource('/src/spec/assets/images/GraphicsSpriteSpec/icon.png'); + const sut = image.toSprite(); + sut.tint = ex.Color.Green; - await image.load(); - await image.ready; + await image.load(); + await image.ready; - ctx.clear(); - sut.draw(ctx, 50 - sut.width / 2, 50 - sut.width / 2); - ctx.flush(); + ctx.clear(); + sut.draw(ctx, 0, 0); + ctx.flush(); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsSpriteSpec/change-size.png'); - }); - - it('can specify the width and height and scale', async () => { - const image = new ex.ImageSource('/src/spec/assets/images/GraphicsTextSpec/spritefont.png', false, ex.ImageFiltering.Pixel); - const sut = new ex.Sprite({ - image, - sourceView: { - x: 0, - y: 0, - width: 16, - height: 16 - } + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsSpriteSpec/icon-tint.png'); }); - sut.width = 64; - sut.height = 64; - - sut.scale = ex.vec(2, 2); - - await image.load(); - await image.ready; - - ctx.clear(); - sut.draw(ctx, 50 - sut.width / 2, 50 - sut.width / 2); - ctx.flush(); - expect(sut.width).toBe(128); - expect(sut.height).toBe(128); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsSpriteSpec/change-size-and-scale.png'); - }); - - it('can specify a source view of an image by default is same dimension as the source', async () => { - const image = new ex.ImageSource('/src/spec/assets/images/GraphicsTextSpec/spritefont.png'); - const sut = new ex.Sprite({ - image, - sourceView: { - x: 0, - y: 0, - width: 16, - height: 16 - } + it('can specify the width and height of a sprite after construction', async () => { + const image = new ex.ImageSource('/src/spec/assets/images/GraphicsTextSpec/spritefont.png', false, ex.ImageFiltering.Pixel); + const sut = new ex.Sprite({ + image, + sourceView: { + x: 0, + y: 0, + width: 16, + height: 16 + } + }); + + sut.width = 64; + sut.height = 64; + + await image.load(); + await image.ready; + + ctx.clear(); + sut.draw(ctx, 50 - sut.width / 2, 50 - sut.width / 2); + ctx.flush(); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsSpriteSpec/change-size.png'); }); - expect(sut.width, 'Graphic width should be 16').toBe(16); - expect(sut.height, 'Graphic height should be 16').toBe(16); - expect(sut.sourceView.x).toBe(0); - expect(sut.sourceView.y).toBe(0); - expect(sut.sourceView.width, 'Graphic sourceView width should be 16').toBe(16); - expect(sut.sourceView.height, 'Graphic sourceView height should be 16').toBe(16); - expect(sut.destSize.width, 'Graphic destSize width should be 16').toBe(16); - expect(sut.destSize.height, 'Graphic destSize height should be 16').toBe(16); - expect(sut.localBounds.width, 'Graphic local bounds width should be 16').toBe(16); - expect(sut.localBounds.height, 'Graphic local bounds height should be 16').toBe(16); - - await image.load(); - await image.ready; - - ctx.clear(); - sut.draw(ctx, 50 - sut.width / 2, 50 - sut.width / 2); - ctx.flush(); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsSpriteSpec/source-view.png'); - }); - - it('can specify a source view of an image and a dest view dimension is destination', async () => { - const image = new ex.ImageSource('/src/spec/assets/images/GraphicsTextSpec/spritefont.png', false, ex.ImageFiltering.Pixel); - const sut = new ex.Sprite({ - image, - sourceView: { - x: 0, - y: 0, - width: 16, - height: 16 - }, - destSize: { - width: 50, - height: 50 - } + it('can specify the width and height and scale', async () => { + const image = new ex.ImageSource('/src/spec/assets/images/GraphicsTextSpec/spritefont.png', false, ex.ImageFiltering.Pixel); + const sut = new ex.Sprite({ + image, + sourceView: { + x: 0, + y: 0, + width: 16, + height: 16 + } + }); + + sut.width = 64; + sut.height = 64; + + sut.scale = ex.vec(2, 2); + + await image.load(); + await image.ready; + + ctx.clear(); + sut.draw(ctx, 50 - sut.width / 2, 50 - sut.width / 2); + ctx.flush(); + expect(sut.width).toBe(128); + expect(sut.height).toBe(128); + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsSpriteSpec/change-size-and-scale.png'); }); - expect(sut.width, 'Graphic width should be 50').toBe(50); - expect(sut.height, 'Graphic height should be 50').toBe(50); - expect(sut.sourceView.x).toBe(0); - expect(sut.sourceView.y).toBe(0); - expect(sut.sourceView.width, 'Graphic sourceView width should be 50').toBe(16); - expect(sut.sourceView.height, 'Graphic sourceView height should be 50').toBe(16); - expect(sut.destSize.width, 'Graphic destSize width should be 50').toBe(50); - expect(sut.destSize.height, 'Graphic destSize height should be 50').toBe(50); - expect(sut.localBounds.width, 'Graphic local bounds width should be 50').toBe(50); - expect(sut.localBounds.height, 'Graphic local bounds height should be 50').toBe(50); - - await image.load(); - await image.ready; - - ctx.clear(); - sut.draw(ctx, 50 - sut.width / 2, 50 - sut.width / 2); - ctx.flush(); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsSpriteSpec/dest-size.png'); - }); - - it('can specify only a dest view dimension, infers native size for source view', async () => { - const image = new ex.ImageSource('/src/spec/assets/images/GraphicsTextSpec/spritefont.png'); - const sut = new ex.Sprite({ - image, - destSize: { - width: 100, - height: 100 - } + it('can specify a source view of an image by default is same dimension as the source', async () => { + const image = new ex.ImageSource('/src/spec/assets/images/GraphicsTextSpec/spritefont.png'); + const sut = new ex.Sprite({ + image, + sourceView: { + x: 0, + y: 0, + width: 16, + height: 16 + } + }); + + expect(sut.width, 'Graphic width should be 16').toBe(16); + expect(sut.height, 'Graphic height should be 16').toBe(16); + expect(sut.sourceView.x).toBe(0); + expect(sut.sourceView.y).toBe(0); + expect(sut.sourceView.width, 'Graphic sourceView width should be 16').toBe(16); + expect(sut.sourceView.height, 'Graphic sourceView height should be 16').toBe(16); + expect(sut.destSize.width, 'Graphic destSize width should be 16').toBe(16); + expect(sut.destSize.height, 'Graphic destSize height should be 16').toBe(16); + expect(sut.localBounds.width, 'Graphic local bounds width should be 16').toBe(16); + expect(sut.localBounds.height, 'Graphic local bounds height should be 16').toBe(16); + + await image.load(); + await image.ready; + + ctx.clear(); + sut.draw(ctx, 50 - sut.width / 2, 50 - sut.width / 2); + ctx.flush(); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsSpriteSpec/source-view.png'); }); - expect(sut.width, 'Graphic width should be 100').toBe(100); - expect(sut.height, 'Graphic height should be 100').toBe(100); - expect(sut.sourceView.x).toBe(0); - expect(sut.sourceView.y).toBe(0); - expect(sut.sourceView.width, 'Graphic sourceView width should be 0 before image load').toBe(0); - expect(sut.sourceView.height, 'Graphic sourceView height should be 0 before image load').toBe(0); - expect(sut.destSize.width, 'Graphic destSize width should be 100').toBe(100); - expect(sut.destSize.height, 'Graphic destSize height should be 100').toBe(100); - expect(sut.localBounds.width, 'Graphic local bounds width should be 100').toBe(100); - expect(sut.localBounds.height, 'Graphic local bounds height should be 100').toBe(100); - - await image.load(); - await image.ready; - - expect(sut.sourceView.width).not.toBe(0); - expect(sut.sourceView.height).not.toBe(0); - - ctx.clear(); - sut.draw(ctx, 50 - sut.width / 2, 50 - sut.width / 2); - ctx.flush(); + it('can specify a source view of an image and a dest view dimension is destination', async () => { + const image = new ex.ImageSource('/src/spec/assets/images/GraphicsTextSpec/spritefont.png', false, ex.ImageFiltering.Pixel); + const sut = new ex.Sprite({ + image, + sourceView: { + x: 0, + y: 0, + width: 16, + height: 16 + }, + destSize: { + width: 50, + height: 50 + } + }); + + expect(sut.width, 'Graphic width should be 50').toBe(50); + expect(sut.height, 'Graphic height should be 50').toBe(50); + expect(sut.sourceView.x).toBe(0); + expect(sut.sourceView.y).toBe(0); + expect(sut.sourceView.width, 'Graphic sourceView width should be 50').toBe(16); + expect(sut.sourceView.height, 'Graphic sourceView height should be 50').toBe(16); + expect(sut.destSize.width, 'Graphic destSize width should be 50').toBe(50); + expect(sut.destSize.height, 'Graphic destSize height should be 50').toBe(50); + expect(sut.localBounds.width, 'Graphic local bounds width should be 50').toBe(50); + expect(sut.localBounds.height, 'Graphic local bounds height should be 50').toBe(50); + + await image.load(); + await image.ready; + + ctx.clear(); + sut.draw(ctx, 50 - sut.width / 2, 50 - sut.width / 2); + ctx.flush(); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsSpriteSpec/dest-size.png'); + }); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsSpriteSpec/dest-view.png'); + it('can specify only a dest view dimension, infers native size for source view', async () => { + const image = new ex.ImageSource('/src/spec/assets/images/GraphicsTextSpec/spritefont.png'); + const sut = new ex.Sprite({ + image, + destSize: { + width: 100, + height: 100 + } + }); + + expect(sut.width, 'Graphic width should be 100').toBe(100); + expect(sut.height, 'Graphic height should be 100').toBe(100); + expect(sut.sourceView.x).toBe(0); + expect(sut.sourceView.y).toBe(0); + expect(sut.sourceView.width, 'Graphic sourceView width should be 0 before image load').toBe(0); + expect(sut.sourceView.height, 'Graphic sourceView height should be 0 before image load').toBe(0); + expect(sut.destSize.width, 'Graphic destSize width should be 100').toBe(100); + expect(sut.destSize.height, 'Graphic destSize height should be 100').toBe(100); + expect(sut.localBounds.width, 'Graphic local bounds width should be 100').toBe(100); + expect(sut.localBounds.height, 'Graphic local bounds height should be 100').toBe(100); + + await image.load(); + await image.ready; + + expect(sut.sourceView.width).not.toBe(0); + expect(sut.sourceView.height).not.toBe(0); + + ctx.clear(); + sut.draw(ctx, 50 - sut.width / 2, 50 - sut.width / 2); + ctx.flush(); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsSpriteSpec/dest-view.png'); + }); }); it('will log one warning if the imagesource is not loaded', () => { diff --git a/src/spec/vitest/TextSpec.ts b/src/spec/vitest/TextSpec.ts index be73905c..d0fcd6da 100644 --- a/src/spec/vitest/TextSpec.ts +++ b/src/spec/vitest/TextSpec.ts @@ -25,32 +25,33 @@ declare global { } } -/** - * - */ -export function waitForFontLoad(font: string, timeout = 2000, interval = 100): Promise { - return new Promise((resolve, reject) => { - // repeatedly poll check - const poller = setInterval(async () => { - try { - await document.fonts.load(font); - } catch (err) { - reject(err); - } - if (document.fonts.check(font)) { - clearInterval(poller); - resolve(true); - } - }, interval); - setTimeout(() => clearInterval(poller), timeout); +function waitForSheet(link: HTMLLinkElement) { + return new Promise((res, rej) => { + link.addEventListener('load', () => res(), { once: true }); + link.addEventListener('error', () => rej(new Error('stylesheet failed to load')), { once: true }); }); } -const isWindows = inject('platform') === 'win32'; -const isLinux = inject('platform') === 'linux'; -const isChromium = inject('browser') === 'chromium'; +async function waitFace(desc: string, text = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789') { + const faces = await document.fonts.load(desc, text); + if (!faces || faces.length === 0) { + throw new Error(`No @font-face matched: ${desc}`); + } + if (!document.fonts.check(desc, text)) { + throw new Error(`Font not ready after load(): ${desc}`); + } +} /** + * Font rendering varies significantly between operating systems and browsers. Even for the same + * OS, the presence of certain kinds of fonts or enabled font features can affect this. So + * we use a low tolerance to at least ensure we don't break rendering altogether. + * + * Below are notes from when we only enabled font screenshot tests on Windows+Chrome. + * It explains some specific differences that may actually be bugs. + * + * ------------------------------------------------------------- + * * for now, only run the Text tests on Windows + Chromium because * there are too many subtle differences in rendering text * across browser/os combos. @@ -72,19 +73,26 @@ const isChromium = inject('browser') === 'chromium'; * all kinds of flags, but text quality differs quite a bit. Can be fixed to pass * some linux screenshots by setting quality of the font to 3, or in one case 10. */ -describe.runIf(isWindows && isChromium)('A Text Graphic', () => { +const IMAGE_TOLERANCE = 0.91; + +describe('A Text Graphic', () => { let canvasElement: HTMLCanvasElement; let ctx: ex.ExcaliburGraphicsContext2DCanvas | ex.ExcaliburGraphicsContextWebGL; beforeAll(async () => { - const fontface = document.createElement('link'); - fontface.href = '/src/spec/assets/images/GraphicsTextSpec/fonts.css'; - fontface.rel = 'stylesheet'; - document.head.appendChild(fontface); - await waitForFontLoad('18px Open Sans'); - await waitForFontLoad('bold 18px Open Sans'); - await waitForFontLoad('italic bold 18px Open Sans'); - await waitForFontLoad('italic 18px Open Sans'); + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = '/src/spec/assets/images/GraphicsTextSpec/fonts.css'; + document.head.appendChild(link); + await waitForSheet(link); + + await Promise.all([ + waitFace('400 1em "Open Sans"'), + waitFace('700 1em "Open Sans"'), + waitFace('italic 400 1em "Open Sans"'), + waitFace('italic 700 1em "Open Sans"') + ]); + await document.fonts.ready; }); @@ -143,256 +151,567 @@ describe.runIf(isWindows && isChromium)('A Text Graphic', () => { expect(sut.localBounds).toEqual(currentBounds.scale(ex.vec(2, 2))); }); - it('can write text', async () => { - const sut = new ex.Text({ - text: 'green text', - color: ex.Color.Green, - font: new ex.Font({ + describe('@visual', () => { + // measurement tests do not screenshot but are still affected by browser rendering, + // so we'll group them in with the visual + it('can have width and height', () => { + const sut = new ex.Text({ + text: 'some extra long text that we want to measure', + color: ex.Color.Green, + font: new ex.Font({ + family: 'Open Sans', + size: 18, + quality: 1, + padding: 0 + }) + }); + + expect(sut.width).toBeCloseTo(386.9, -1); + expect(sut.height).toBeCloseTo(18, 0); + expect(sut.localBounds.width).toBeCloseTo(386.9, -1); + expect(sut.localBounds.height).toBeCloseTo(18, 0); + }); + + it('can measure text for a font', () => { + const sut = new ex.Font({ family: 'Open Sans', size: 18, quality: 1, - padding: 0, - baseAlign: ex.BaseAlign.Alphabetic - }) + padding: 0 + }); + + const bounds = sut.measureText('some extra long text that we want to measure'); + + expect(bounds.width).toBeCloseTo(386.9, -1); + expect(bounds.height).toBeCloseTo(18, 0); }); + it('can write text', async () => { + const sut = new ex.Text({ + text: 'green text', + color: ex.Color.Green, + font: new ex.Font({ + family: 'Open Sans', + size: 18, + quality: 1, + padding: 0, + baseAlign: ex.BaseAlign.Alphabetic + }) + }); - sut.draw(ctx, 10, 50); + sut.draw(ctx, 10, 50); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/text.png'); - }); + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/text.png', IMAGE_TOLERANCE); + }); - it('can draw multiple lines of text (font)', async () => { - const sut = new ex.Text({ - text: 'multiple\nlines\nof text', - color: ex.Color.Green, - font: new ex.Font({ - family: 'Open Sans', - size: 18, - lineHeight: 18, - quality: 1, - padding: 0, - baseAlign: ex.BaseAlign.Alphabetic - }) + it('can draw multiple lines of text (font)', async () => { + const sut = new ex.Text({ + text: 'multiple\nlines\nof text', + color: ex.Color.Green, + font: new ex.Font({ + family: 'Open Sans', + size: 18, + lineHeight: 18, + quality: 1, + padding: 0, + baseAlign: ex.BaseAlign.Alphabetic + }) + }); + + sut.draw(ctx, 10, 20); + + expect(sut.width).toBeCloseTo(69.8, -1); + expect(sut.localBounds.width).toBeCloseTo(69.8, -1); + expect(sut.height).toBeCloseTo(18 * 3, 0); + expect(sut.localBounds.height).toBeCloseTo(18 * 3, 0); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/multi-text-win.png', IMAGE_TOLERANCE); }); - sut.draw(ctx, 10, 20); + it('can flip text vertically and horizontally', async () => { + const sut = new ex.Text({ + text: 'green text', + color: ex.Color.Green, + font: new ex.Font({ + family: 'Open Sans', + size: 18, + quality: 1, + padding: 0, + baseAlign: ex.BaseAlign.Alphabetic + }) + }); + + sut.flipHorizontal = true; + sut.flipVertical = true; + sut.draw(ctx, 10, 50); - expect(sut.width).toBeCloseTo(69.8, -1); - expect(sut.localBounds.width).toBeCloseTo(69.8, -1); - expect(sut.height).toBeCloseTo(18 * 3, 0); - expect(sut.localBounds.height).toBeCloseTo(18 * 3, 0); + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/flipped.png', IMAGE_TOLERANCE); + }); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/multi-text-win.png'); - }); + it('can align fonts and reuse a font', async () => { + const sut = new ex.Text({ + text: 'green text', + color: ex.Color.Green, + font: new ex.Font({ + family: 'Open Sans', + size: 18, + quality: 1, + padding: 0, + baseAlign: ex.BaseAlign.Alphabetic + }) + }); - it('can have width and height', () => { - const sut = new ex.Text({ - text: 'some extra long text that we want to measure', - color: ex.Color.Green, - font: new ex.Font({ - family: 'Open Sans', - size: 18, - quality: 1, - padding: 0 - }) + sut.draw(ctx, 10, 20); + sut.draw(ctx, 10, 40); + sut.draw(ctx, 10, 60); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/font-alignment.png', IMAGE_TOLERANCE); }); - expect(sut.width).toBeCloseTo(386.9, -1); - expect(sut.height).toBeCloseTo(18, 0); - expect(sut.localBounds.width).toBeCloseTo(386.9, -1); - expect(sut.localBounds.height).toBeCloseTo(18, 0); - }); + it('can rotate text around the middle', async () => { + const sut = new ex.Text({ + text: 'green text', + color: ex.Color.Green, + font: new ex.Font({ + family: 'Open Sans', + size: 18, + quality: 1, + padding: 0, + baseAlign: ex.BaseAlign.Alphabetic + }) + }); - it('can measure text for a font', () => { - const sut = new ex.Font({ - family: 'Open Sans', - size: 18, - quality: 1, - padding: 0 + sut.rotation = Math.PI / 2; + sut.draw(ctx, 10, 50); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/rotated.png', IMAGE_TOLERANCE); }); - const bounds = sut.measureText('some extra long text that we want to measure'); + it('can rotate text around the left', async () => { + const sut = new ex.Text({ + text: 'green text', + color: ex.Color.Green, + font: new ex.Font({ + family: 'Open Sans', + size: 18, + quality: 1, + padding: 0, + baseAlign: ex.BaseAlign.Alphabetic + }) + }); - expect(bounds.width).toBeCloseTo(386.9, -1); - expect(bounds.height).toBeCloseTo(18, 0); - }); + sut.origin = ex.Vector.Zero; + sut.rotation = Math.PI / 2; + sut.draw(ctx, 10, 50); - it('can flip text vertically and horizontally', async () => { - const sut = new ex.Text({ - text: 'green text', - color: ex.Color.Green, - font: new ex.Font({ - family: 'Open Sans', - size: 18, - quality: 1, - padding: 0, - baseAlign: ex.BaseAlign.Alphabetic - }) + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/rotated-left.png', IMAGE_TOLERANCE); }); - sut.flipHorizontal = true; - sut.flipVertical = true; - sut.draw(ctx, 10, 50); + it('can rotate text around the right', async () => { + const sut = new ex.Text({ + text: 'green text', + color: ex.Color.Green, + font: new ex.Font({ + family: 'Open Sans', + size: 18, + quality: 1, + padding: 0, + baseAlign: ex.BaseAlign.Alphabetic + }) + }); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/flipped.png'); - }); + sut.origin = ex.vec(sut.width, 0); + sut.rotation = -Math.PI / 2; + sut.draw(ctx, 10, 50); - it('can align fonts and reuse a font', async () => { - const sut = new ex.Text({ - text: 'green text', - color: ex.Color.Green, - font: new ex.Font({ - family: 'Open Sans', - size: 18, - quality: 1, - padding: 0, - baseAlign: ex.BaseAlign.Alphabetic - }) + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/rotated-right.png', IMAGE_TOLERANCE); }); - sut.draw(ctx, 10, 20); - sut.draw(ctx, 10, 40); - sut.draw(ctx, 10, 60); + it('can be bold', async () => { + const sut = new ex.Text({ + text: 'green text', + color: ex.Color.Green, + font: new ex.Font({ + family: 'Open Sans', + bold: true, + size: 18, + quality: 1, + padding: 0, + baseAlign: ex.BaseAlign.Alphabetic + }) + }); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/font-alignment.png'); - }); + sut.draw(ctx, 10, 50); - it('can rotate text around the middle', async () => { - const sut = new ex.Text({ - text: 'green text', - color: ex.Color.Green, - font: new ex.Font({ - family: 'Open Sans', - size: 18, - quality: 1, - padding: 0, - baseAlign: ex.BaseAlign.Alphabetic - }) + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/bold-win.png', IMAGE_TOLERANCE); }); - sut.rotation = Math.PI / 2; - sut.draw(ctx, 10, 50); + it('can be italic', async () => { + const sut = new ex.Text({ + text: 'green text', + color: ex.Color.Green, + font: new ex.Font({ + family: 'Open Sans', + style: ex.FontStyle.Italic, + size: 18, + quality: 1, + padding: 0, + baseAlign: ex.BaseAlign.Alphabetic + }) + }); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/rotated.png'); - }); + sut.draw(ctx, 10, 50); - it('can rotate text around the left', async () => { - const sut = new ex.Text({ - text: 'green text', - color: ex.Color.Green, - font: new ex.Font({ - family: 'Open Sans', - size: 18, - quality: 1, - padding: 0, - baseAlign: ex.BaseAlign.Alphabetic - }) + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/italic.png', IMAGE_TOLERANCE); }); - sut.origin = ex.Vector.Zero; - sut.rotation = Math.PI / 2; - sut.draw(ctx, 10, 50); + it('can have line height', async () => { + const sut = new ex.Text({ + text: 'green text\nthat has multiple\nlines to it.', + color: ex.Color.Green, + font: new ex.Font({ + family: 'Open Sans', + size: 18, + quality: 1, + lineHeight: 36, + baseAlign: ex.BaseAlign.Alphabetic + }) + }); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/rotated-left.png'); - }); + sut.draw(ctx, 10, 10); - it('can rotate text around the right', async () => { - const sut = new ex.Text({ - text: 'green text', - color: ex.Color.Green, - font: new ex.Font({ - family: 'Open Sans', - size: 18, - quality: 1, - padding: 0, - baseAlign: ex.BaseAlign.Alphabetic - }) + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/line-height-win.png', IMAGE_TOLERANCE); }); - sut.origin = ex.vec(sut.width, 0); - sut.rotation = -Math.PI / 2; - sut.draw(ctx, 10, 50); + it('can have a shadow', async () => { + const sut = new ex.Text({ + text: 'green text', + color: ex.Color.Green, + font: new ex.Font({ + family: 'Open Sans', + size: 18, + quality: 1, + padding: 0, + baseAlign: ex.BaseAlign.Alphabetic, + shadow: { + blur: 5, + offset: ex.vec(4, 4), + color: ex.Color.Blue + } + }) + }); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/rotated-right.png'); - }); + sut.draw(ctx, 10, 50); - it('can be bold', async () => { - const sut = new ex.Text({ - text: 'green text', - color: ex.Color.Green, - font: new ex.Font({ + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/shadow.png', IMAGE_TOLERANCE); + }); + + it('can reuse a font', async () => { + const sut = new ex.Font({ family: 'Open Sans', - bold: true, size: 18, quality: 1, - padding: 0, baseAlign: ex.BaseAlign.Alphabetic - }) + }); + + const text1 = new ex.Text({ + text: 'text111', + font: sut + }); + + const text2 = new ex.Text({ + text: 'text222', + font: sut + }); + + text1.draw(ctx, 10, 20); + text2.draw(ctx, 10, 40); + ctx.flush(); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/reuse-font-win.png', IMAGE_TOLERANCE); }); - sut.draw(ctx, 10, 50); + it('can draw large pieces of text', async () => { + const sut = new ex.Text({ + text: + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + + 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + + 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n', + font: new ex.Font({ + family: 'Open Sans', + size: 18, + quality: 4, + padding: 0, + baseAlign: ex.BaseAlign.Alphabetic + }) + }); + canvasElement.remove(); + canvasElement = document.createElement('canvas'); + canvasElement.width = 1000; + canvasElement.height = 1000; - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/bold-win.png'); - }); + ctx = new ex.ExcaliburGraphicsContextWebGL({ canvasElement, snapToPixel: false }); + ctx.clear(); - it('can be italic', async () => { - const sut = new ex.Text({ - text: 'green text', - color: ex.Color.Green, - font: new ex.Font({ - family: 'Open Sans', - style: ex.FontStyle.Italic, - size: 18, - quality: 1, - padding: 0, - baseAlign: ex.BaseAlign.Alphabetic - }) + sut.draw(ctx, 10, 50); + ctx.flush(); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/long-text.png', IMAGE_TOLERANCE); }); - sut.draw(ctx, 10, 50); + it('can do some simple shadowing', async () => { + const spriteFontImage = new ex.ImageSource('/src/spec/assets/images/GraphicsTextSpec/spritefont.png'); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/italic.png'); - }); + await spriteFontImage.load(); - it('can have line height', async () => { - const sut = new ex.Text({ - text: 'green text\nthat has multiple\nlines to it.', - color: ex.Color.Green, - font: new ex.Font({ - family: 'Open Sans', - size: 18, - quality: 1, - lineHeight: 36, - baseAlign: ex.BaseAlign.Alphabetic - }) + const spriteFontSheet = ex.SpriteSheet.fromImageSource({ + image: spriteFontImage, + grid: { + rows: 3, + columns: 16, + spriteWidth: 16, + spriteHeight: 16 + } + }); + + const spriteFont = new ex.SpriteFont({ + alphabet: '0123456789abcdefghijklmnopqrstuvwxyz,!\'&."?- ', + caseInsensitive: true, + spacing: -5, + spriteSheet: spriteFontSheet, + shadow: { + offset: ex.vec(3, 3) + } + }); + + const sut = new ex.Text({ + text: 'Some Sprite Text!?', + font: spriteFont + }); + + canvasElement.width = 200; + ctx.clear(); + sut.draw(ctx, 0, 50); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/spritefont-shadow.png', IMAGE_TOLERANCE); }); - sut.draw(ctx, 10, 10); + it('can rotate spritefont text around the middle', async () => { + const spriteFontImage = new ex.ImageSource('/src/spec/assets/images/GraphicsTextSpec/spritefont.png'); + await spriteFontImage.load(); + const spriteFontSheet = ex.SpriteSheet.fromImageSource({ + image: spriteFontImage, + grid: { + rows: 3, + columns: 16, + spriteWidth: 16, + spriteHeight: 16 + } + }); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/line-height-win.png'); - }); + const spriteFont = new ex.SpriteFont({ + alphabet: '0123456789abcdefghijklmnopqrstuvwxyz,!\'&."?- ', + caseInsensitive: true, + spriteSheet: spriteFontSheet, + spacing: -6 + }); + const sut = new ex.Text({ + text: 'some text', + font: spriteFont + }); - it('can have a shadow', async () => { - const sut = new ex.Text({ - text: 'green text', - color: ex.Color.Green, - font: new ex.Font({ + canvasElement.width = 100; + ctx.clear(); + + sut.rotation = Math.PI / 2; + sut.draw(ctx, 10, 40); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/rotated-spritefont.png', IMAGE_TOLERANCE); + }); + + it('can align fonts and reuse a spritefont', async () => { + const spriteFontImage = new ex.ImageSource('/src/spec/assets/images/GraphicsTextSpec/spritefont.png'); + await spriteFontImage.load(); + const spriteFontSheet = ex.SpriteSheet.fromImageSource({ + image: spriteFontImage, + grid: { + rows: 3, + columns: 16, + spriteWidth: 16, + spriteHeight: 16 + } + }); + + const spriteFont = new ex.SpriteFont({ + alphabet: '0123456789abcdefghijklmnopqrstuvwxyz,!\'&."?- ', + caseInsensitive: true, + spriteSheet: spriteFontSheet, + spacing: -6 + }); + const sut = new ex.Text({ + text: 'some text', + font: spriteFont + }); + + canvasElement.width = 100; + ctx.clear(); + + sut.draw(ctx, 0, 20); + sut.draw(ctx, 0, 40); + sut.draw(ctx, 0, 60); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/spritefont-alignment.png', IMAGE_TOLERANCE); + }); + + it('can draw multiple lines of text (spritefont)', async () => { + const spriteFontImage = new ex.ImageSource('/src/spec/assets/images/GraphicsTextSpec/spritefont.png'); + await spriteFontImage.load(); + const spriteFontSheet = ex.SpriteSheet.fromImageSource({ + image: spriteFontImage, + grid: { + rows: 3, + columns: 16, + spriteWidth: 16, + spriteHeight: 16 + } + }); + + const spriteFont = new ex.SpriteFont({ + alphabet: '0123456789abcdefghijklmnopqrstuvwxyz,!\'&."?- ', + caseInsensitive: true, + spriteSheet: spriteFontSheet, + spacing: -6 + }); + const sut = new ex.Text({ + text: 'multiple\nlines\nof text', + color: ex.Color.Green, + font: spriteFont + }); + + canvasElement.width = 100; + ctx.clear(); + + sut.draw(ctx, 10, 20); + + expect(sut.width).toBeCloseTo(80); + expect(sut.height).toBeCloseTo(48); + expect(sut.localBounds.width).toBeCloseTo(80); + expect(sut.localBounds.height).toBeCloseTo(48); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/multi-text-spritefont.png', IMAGE_TOLERANCE); + }); + + it('can word wrap text for a normal font', async () => { + const sut = new ex.Font({ family: 'Open Sans', size: 18, quality: 1, - padding: 0, - baseAlign: ex.BaseAlign.Alphabetic, - shadow: { - blur: 5, - offset: ex.vec(4, 4), - color: ex.Color.Blue - } - }) - }); + baseAlign: ex.BaseAlign.Alphabetic + }); + + const text1 = new ex.Text({ + text: 'some super long text that should wrap after 100 pixels', + font: sut, + maxWidth: 100 + }); + + canvasElement.width = 100; + ctx = new ex.ExcaliburGraphicsContext2DCanvas({ canvasElement, multiSampleAntialiasing: false }); - sut.draw(ctx, 10, 50); + ctx.clear(); + text1.draw(ctx, 0, 18); + ctx.flush(); + + await new Promise((r) => setTimeout(r, 1000)); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/shadow.png'); + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/font-text-wrap-win.png', IMAGE_TOLERANCE); + }); }); it('can force clear text bitmap cache', () => { @@ -528,31 +847,6 @@ describe.runIf(isWindows && isChromium)('A Text Graphic', () => { expect(ex.FontCache.cacheSize).toBe(2); }); - it('can reuse a font', async () => { - const sut = new ex.Font({ - family: 'Open Sans', - size: 18, - quality: 1, - baseAlign: ex.BaseAlign.Alphabetic - }); - - const text1 = new ex.Text({ - text: 'text111', - font: sut - }); - - const text2 = new ex.Text({ - text: 'text222', - font: sut - }); - - text1.draw(ctx, 10, 20); - text2.draw(ctx, 10, 40); - ctx.flush(); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/reuse-font-win.png'); - }); - it('can get text dimension before drawing', () => { const sut = new ex.Font({ family: 'Open Sans', @@ -575,121 +869,6 @@ describe.runIf(isWindows && isChromium)('A Text Graphic', () => { expect(text.height).not.toBe(0); }); - it('can draw large pieces of text', async () => { - const sut = new ex.Text({ - text: - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n' + - 'A long piece of text that is multiple lines needs to be broken into multiple pieces ' + - 'so it can be drawn on mobile devices correctly otherwise it will draw black rectangles on the screen\n', - font: new ex.Font({ - family: 'Open Sans', - size: 18, - quality: 4, - padding: 0, - baseAlign: ex.BaseAlign.Alphabetic - }) - }); - canvasElement.remove(); - canvasElement = document.createElement('canvas'); - canvasElement.width = 1000; - canvasElement.height = 1000; - - ctx = new ex.ExcaliburGraphicsContextWebGL({ canvasElement, snapToPixel: false }); - ctx.clear(); - - sut.draw(ctx, 10, 50); - ctx.flush(); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/long-text.png'); - }); - it('can create lots of text without crash', () => { expect(() => { const text: ex.Text[] = []; @@ -748,7 +927,10 @@ describe.runIf(isWindows && isChromium)('A Text Graphic', () => { expect(sut.font).not.toBe(clone.font); }); - it('can specify a spritefont', async () => { + it('will log warnings when there are issues', async () => { + const logger = ex.Logger.getInstance(); + const warnSpy = vi.spyOn(logger, 'warnOnce'); + const spriteFontImage = new ex.ImageSource('/src/spec/assets/images/GraphicsTextSpec/spritefont.png'); await spriteFontImage.load(); @@ -756,7 +938,7 @@ describe.runIf(isWindows && isChromium)('A Text Graphic', () => { const spriteFontSheet = ex.SpriteSheet.fromImageSource({ image: spriteFontImage, grid: { - rows: 3, + rows: 1, columns: 16, spriteWidth: 16, spriteHeight: 16 @@ -771,20 +953,31 @@ describe.runIf(isWindows && isChromium)('A Text Graphic', () => { }); const sut = new ex.Text({ - text: 'Some Sprite Text!?', + text: 'a', font: spriteFont }); - sut.draw(ctx, 0, 50); + sut.text = '~'; + sut.draw(ctx, 0, 0); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/spritefont-text.png'); + expect(warnSpy.mock.calls[0]).toEqual([ + "SpriteFont - Cannot find letter '~' in configured alphabet '0123456789abcdefghijklmnopqrstuvwxyz,!'&.\"?- '." + ]); + expect(warnSpy.mock.calls[1]).toEqual([ + 'There maybe be more issues in the SpriteFont configuration. No additional warnings will be logged.' + ]); + warnSpy.mockReset(); + sut.text = '?'; + sut.draw(ctx, 0, 0); + expect(warnSpy.mock.calls[0]).toEqual(["SpriteFont - Cannot find sprite for '?' at index '42' in configured SpriteSheet"]); + expect(warnSpy.mock.calls[1]).toEqual([ + 'There maybe be more issues in the SpriteFont configuration. No additional warnings will be logged.' + ]); }); - it('can scale a spritefont', async () => { + it('can measure text for a spritefont', async () => { const spriteFontImage = new ex.ImageSource('/src/spec/assets/images/GraphicsTextSpec/spritefont.png'); - await spriteFontImage.load(); - const spriteFontSheet = ex.SpriteSheet.fromImageSource({ image: spriteFontImage, grid: { @@ -795,331 +988,151 @@ describe.runIf(isWindows && isChromium)('A Text Graphic', () => { } }); - const spriteFont = new ex.SpriteFont({ + const sut = new ex.SpriteFont({ alphabet: '0123456789abcdefghijklmnopqrstuvwxyz,!\'&."?- ', caseInsensitive: true, - spacing: -5, spriteSheet: spriteFontSheet, - scale: ex.vec(3, 3) - }); - - const sut = new ex.Text({ - text: 'Some Sprite Text!?', - font: spriteFont + spacing: -6 }); - sut.draw(ctx, 0, 50); - expect(spriteFont.measureText('some test')).toEqual(ex.BoundingBox.fromDimension((16 - 5) * 9 * 3, 16 * 3, ex.vec(0, 0))); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/spritefont-scaled.png'); + const bounds = sut.measureText('some extra long text that we want to measure'); + expect(bounds.width).toBeCloseTo(440, -1); + expect(bounds.height).toBeCloseTo(16, 0); }); - it('can have a custom lineHeight', async () => { - const spriteFontImage = new ex.ImageSource('/src/spec/assets/images/GraphicsTextSpec/spritefont.png'); - - await spriteFontImage.load(); - - const spriteFontSheet = ex.SpriteSheet.fromImageSource({ - image: spriteFontImage, - grid: { - rows: 3, - columns: 16, - spriteWidth: 16, - spriteHeight: 16 - } + describe('@visual', () => { + it('can specify a spritefont', async () => { + const spriteFontImage = new ex.ImageSource('/src/spec/assets/images/GraphicsTextSpec/spritefont.png'); + + await spriteFontImage.load(); + + const spriteFontSheet = ex.SpriteSheet.fromImageSource({ + image: spriteFontImage, + grid: { + rows: 3, + columns: 16, + spriteWidth: 16, + spriteHeight: 16 + } + }); + + const spriteFont = new ex.SpriteFont({ + alphabet: '0123456789abcdefghijklmnopqrstuvwxyz,!\'&."?- ', + caseInsensitive: true, + spacing: -5, + spriteSheet: spriteFontSheet + }); + + const sut = new ex.Text({ + text: 'Some Sprite Text!?', + font: spriteFont + }); + + sut.draw(ctx, 0, 50); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/spritefont-text.png'); }); - const spriteFont = new ex.SpriteFont({ - alphabet: '0123456789abcdefghijklmnopqrstuvwxyz,!\'&."?- ', - caseInsensitive: true, - spacing: -5, - spriteSheet: spriteFontSheet, - lineHeight: 32 + it('can scale a spritefont', async () => { + const spriteFontImage = new ex.ImageSource('/src/spec/assets/images/GraphicsTextSpec/spritefont.png'); + + await spriteFontImage.load(); + + const spriteFontSheet = ex.SpriteSheet.fromImageSource({ + image: spriteFontImage, + grid: { + rows: 3, + columns: 16, + spriteWidth: 16, + spriteHeight: 16 + } + }); + + const spriteFont = new ex.SpriteFont({ + alphabet: '0123456789abcdefghijklmnopqrstuvwxyz,!\'&."?- ', + caseInsensitive: true, + spacing: -5, + spriteSheet: spriteFontSheet, + scale: ex.vec(3, 3) + }); + + const sut = new ex.Text({ + text: 'Some Sprite Text!?', + font: spriteFont + }); + + sut.draw(ctx, 0, 50); + expect(spriteFont.measureText('some test')).toEqual(ex.BoundingBox.fromDimension((16 - 5) * 9 * 3, 16 * 3, ex.vec(0, 0))); + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/spritefont-scaled.png'); }); - const sut = new ex.Text({ - text: '111\n222\n333\n444', - font: spriteFont + it('can have a custom lineHeight', async () => { + const spriteFontImage = new ex.ImageSource('/src/spec/assets/images/GraphicsTextSpec/spritefont.png'); + + await spriteFontImage.load(); + + const spriteFontSheet = ex.SpriteSheet.fromImageSource({ + image: spriteFontImage, + grid: { + rows: 3, + columns: 16, + spriteWidth: 16, + spriteHeight: 16 + } + }); + + const spriteFont = new ex.SpriteFont({ + alphabet: '0123456789abcdefghijklmnopqrstuvwxyz,!\'&."?- ', + caseInsensitive: true, + spacing: -5, + spriteSheet: spriteFontSheet, + lineHeight: 32 + }); + + const sut = new ex.Text({ + text: '111\n222\n333\n444', + font: spriteFont + }); + + sut.draw(ctx, 0, 0); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/sprite-font-line-height.png', IMAGE_TOLERANCE); }); - sut.draw(ctx, 0, 0); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/sprite-font-line-height.png'); - }); - }); - - it('will log warnings when there are issues', async () => { - const logger = ex.Logger.getInstance(); - const warnSpy = vi.spyOn(logger, 'warnOnce'); - - const spriteFontImage = new ex.ImageSource('/src/spec/assets/images/GraphicsTextSpec/spritefont.png'); - - await spriteFontImage.load(); - - const spriteFontSheet = ex.SpriteSheet.fromImageSource({ - image: spriteFontImage, - grid: { - rows: 1, - columns: 16, - spriteWidth: 16, - spriteHeight: 16 - } - }); - - const spriteFont = new ex.SpriteFont({ - alphabet: '0123456789abcdefghijklmnopqrstuvwxyz,!\'&."?- ', - caseInsensitive: true, - spacing: -5, - spriteSheet: spriteFontSheet - }); - - const sut = new ex.Text({ - text: 'a', - font: spriteFont - }); - - sut.text = '~'; - sut.draw(ctx, 0, 0); - - expect(warnSpy.mock.calls[0]).toEqual([ - "SpriteFont - Cannot find letter '~' in configured alphabet '0123456789abcdefghijklmnopqrstuvwxyz,!'&.\"?- '." - ]); - expect(warnSpy.mock.calls[1]).toEqual([ - 'There maybe be more issues in the SpriteFont configuration. No additional warnings will be logged.' - ]); - warnSpy.mockReset(); - sut.text = '?'; - sut.draw(ctx, 0, 0); - expect(warnSpy.mock.calls[0]).toEqual(["SpriteFont - Cannot find sprite for '?' at index '42' in configured SpriteSheet"]); - expect(warnSpy.mock.calls[1]).toEqual([ - 'There maybe be more issues in the SpriteFont configuration. No additional warnings will be logged.' - ]); - }); - - it('can do some simple shadowing', async () => { - const spriteFontImage = new ex.ImageSource('/src/spec/assets/images/GraphicsTextSpec/spritefont.png'); - - await spriteFontImage.load(); - - const spriteFontSheet = ex.SpriteSheet.fromImageSource({ - image: spriteFontImage, - grid: { - rows: 3, - columns: 16, - spriteWidth: 16, - spriteHeight: 16 - } - }); - - const spriteFont = new ex.SpriteFont({ - alphabet: '0123456789abcdefghijklmnopqrstuvwxyz,!\'&."?- ', - caseInsensitive: true, - spacing: -5, - spriteSheet: spriteFontSheet, - shadow: { - offset: ex.vec(3, 3) - } - }); - - const sut = new ex.Text({ - text: 'Some Sprite Text!?', - font: spriteFont - }); - - canvasElement.width = 200; - ctx.clear(); - sut.draw(ctx, 0, 50); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/spritefont-shadow.png'); - }); - - it('can rotate spritefont text around the middle', async () => { - const spriteFontImage = new ex.ImageSource('/src/spec/assets/images/GraphicsTextSpec/spritefont.png'); - await spriteFontImage.load(); - const spriteFontSheet = ex.SpriteSheet.fromImageSource({ - image: spriteFontImage, - grid: { - rows: 3, - columns: 16, - spriteWidth: 16, - spriteHeight: 16 - } - }); - - const spriteFont = new ex.SpriteFont({ - alphabet: '0123456789abcdefghijklmnopqrstuvwxyz,!\'&."?- ', - caseInsensitive: true, - spriteSheet: spriteFontSheet, - spacing: -6 - }); - const sut = new ex.Text({ - text: 'some text', - font: spriteFont - }); - - canvasElement.width = 100; - ctx.clear(); - - sut.rotation = Math.PI / 2; - sut.draw(ctx, 10, 40); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/rotated-spritefont.png'); - }); - - it('can align fonts and reuse a spritefont', async () => { - const spriteFontImage = new ex.ImageSource('/src/spec/assets/images/GraphicsTextSpec/spritefont.png'); - await spriteFontImage.load(); - const spriteFontSheet = ex.SpriteSheet.fromImageSource({ - image: spriteFontImage, - grid: { - rows: 3, - columns: 16, - spriteWidth: 16, - spriteHeight: 16 - } - }); - - const spriteFont = new ex.SpriteFont({ - alphabet: '0123456789abcdefghijklmnopqrstuvwxyz,!\'&."?- ', - caseInsensitive: true, - spriteSheet: spriteFontSheet, - spacing: -6 - }); - const sut = new ex.Text({ - text: 'some text', - font: spriteFont - }); - - canvasElement.width = 100; - ctx.clear(); - - sut.draw(ctx, 0, 20); - sut.draw(ctx, 0, 40); - sut.draw(ctx, 0, 60); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/spritefont-alignment.png'); - }); - - it('can draw multiple lines of text (spritefont)', async () => { - const spriteFontImage = new ex.ImageSource('/src/spec/assets/images/GraphicsTextSpec/spritefont.png'); - await spriteFontImage.load(); - const spriteFontSheet = ex.SpriteSheet.fromImageSource({ - image: spriteFontImage, - grid: { - rows: 3, - columns: 16, - spriteWidth: 16, - spriteHeight: 16 - } - }); - - const spriteFont = new ex.SpriteFont({ - alphabet: '0123456789abcdefghijklmnopqrstuvwxyz,!\'&."?- ', - caseInsensitive: true, - spriteSheet: spriteFontSheet, - spacing: -6 - }); - const sut = new ex.Text({ - text: 'multiple\nlines\nof text', - color: ex.Color.Green, - font: spriteFont - }); - - canvasElement.width = 100; - ctx.clear(); - - sut.draw(ctx, 10, 20); - - expect(sut.width).toBeCloseTo(80); - expect(sut.height).toBeCloseTo(48); - expect(sut.localBounds.width).toBeCloseTo(80); - expect(sut.localBounds.height).toBeCloseTo(48); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/multi-text-spritefont.png'); - }); - - it('can measure text for a spritefont', async () => { - const spriteFontImage = new ex.ImageSource('/src/spec/assets/images/GraphicsTextSpec/spritefont.png'); - await spriteFontImage.load(); - const spriteFontSheet = ex.SpriteSheet.fromImageSource({ - image: spriteFontImage, - grid: { - rows: 3, - columns: 16, - spriteWidth: 16, - spriteHeight: 16 - } - }); - - const sut = new ex.SpriteFont({ - alphabet: '0123456789abcdefghijklmnopqrstuvwxyz,!\'&."?- ', - caseInsensitive: true, - spriteSheet: spriteFontSheet, - spacing: -6 - }); - - const bounds = sut.measureText('some extra long text that we want to measure'); - expect(bounds.width).toBeCloseTo(440, -1); - expect(bounds.height).toBeCloseTo(16, 0); - }); - - it('can word wrap text for a spritefont', async () => { - const spriteFontImage = new ex.ImageSource('/src/spec/assets/images/GraphicsTextSpec/spritefont.png'); - await spriteFontImage.load(); - const spriteFontSheet = ex.SpriteSheet.fromImageSource({ - image: spriteFontImage, - grid: { - rows: 3, - columns: 16, - spriteWidth: 16, - spriteHeight: 16 - } - }); - - const spriteFont = new ex.SpriteFont({ - alphabet: '0123456789abcdefghijklmnopqrstuvwxyz,!\'&."?- ', - caseInsensitive: true, - spriteSheet: spriteFontSheet, - spacing: -4 - }); - - const sut = new ex.Text({ - text: 'some super long text that should wrap after 100 pixels', - color: ex.Color.Green, - font: spriteFont, - maxWidth: 100 - }); - - canvasElement.width = 100; - ctx.clear(); - - sut.draw(ctx, 0, 0); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/sprite-font-text-wrap.png'); - }); - - it('can word wrap text for a normal font', async () => { - const sut = new ex.Font({ - family: 'Open Sans', - size: 18, - quality: 1, - baseAlign: ex.BaseAlign.Alphabetic - }); - - const text1 = new ex.Text({ - text: 'some super long text that should wrap after 100 pixels', - font: sut, - maxWidth: 100 + it('can word wrap text for a spritefont', async () => { + const spriteFontImage = new ex.ImageSource('/src/spec/assets/images/GraphicsTextSpec/spritefont.png'); + await spriteFontImage.load(); + const spriteFontSheet = ex.SpriteSheet.fromImageSource({ + image: spriteFontImage, + grid: { + rows: 3, + columns: 16, + spriteWidth: 16, + spriteHeight: 16 + } + }); + + const spriteFont = new ex.SpriteFont({ + alphabet: '0123456789abcdefghijklmnopqrstuvwxyz,!\'&."?- ', + caseInsensitive: true, + spriteSheet: spriteFontSheet, + spacing: -4 + }); + + const sut = new ex.Text({ + text: 'some super long text that should wrap after 100 pixels', + color: ex.Color.Green, + font: spriteFont, + maxWidth: 100 + }); + + canvasElement.width = 100; + ctx.clear(); + + sut.draw(ctx, 0, 0); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/sprite-font-text-wrap.png', IMAGE_TOLERANCE); + }); }); - - canvasElement.width = 100; - ctx = new ex.ExcaliburGraphicsContext2DCanvas({ canvasElement, multiSampleAntialiasing: false }); - - ctx.clear(); - text1.draw(ctx, 0, 18); - ctx.flush(); - - await new Promise((r) => setTimeout(r, 1000)); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/GraphicsTextSpec/font-text-wrap-win.png'); }); }); diff --git a/src/spec/vitest/TileMapSpec.ts b/src/spec/vitest/TileMapSpec.ts index 3183d6fc..1224997c 100644 --- a/src/spec/vitest/TileMapSpec.ts +++ b/src/spec/vitest/TileMapSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; import { BodyComponent } from '@excalibur'; import { ColliderComponent } from '../../engine/Collision/ColliderComponent'; @@ -177,214 +177,216 @@ describe('A TileMap', () => { expect(otherCell2.data.get('some_vale')).not.toBeDefined(); }); - it('can use arbitrary graphics', async () => { - const tm = new ex.TileMap({ - pos: ex.vec(0, 0), - tileWidth: 32, - tileHeight: 32, - rows: 3, - columns: 5 - }); - tm._initialize(engine); - tm.update(engine, 99); - - const cell = tm.getTile(0, 0); - const rectangle = new ex.Rectangle({ - width: 32, - height: 32, - color: ex.Color.Red - }); - const circle = new ex.Circle({ - radius: 16, - color: ex.Color.Blue - }); - const animation = new ex.Animation({ - frames: [ - { graphic: rectangle, duration: 100 }, - { graphic: circle, duration: 100 } - ] - }); - - cell.addGraphic(animation); + describe('@visual', () => { + it('can use arbitrary graphics', async () => { + const tm = new ex.TileMap({ + pos: ex.vec(0, 0), + tileWidth: 32, + tileHeight: 32, + rows: 3, + columns: 5 + }); + tm._initialize(engine); + tm.update(engine, 99); + + const cell = tm.getTile(0, 0); + const rectangle = new ex.Rectangle({ + width: 32, + height: 32, + color: ex.Color.Red + }); + const circle = new ex.Circle({ + radius: 16, + color: ex.Color.Blue + }); + const animation = new ex.Animation({ + frames: [ + { graphic: rectangle, duration: 100 }, + { graphic: circle, duration: 100 } + ] + }); - drawWithTransform(engine.graphicsContext, tm, 99); - engine.graphicsContext.flush(); + cell.addGraphic(animation); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/TileMapSpec/TileMapGraphicSquare.png'); + drawWithTransform(engine.graphicsContext, tm, 99); + engine.graphicsContext.flush(); - tm.update(engine, 99); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/TileMapSpec/TileMapGraphicSquare.png'); - drawWithTransform(engine.graphicsContext, tm, 99); - engine.graphicsContext.flush(); + tm.update(engine, 99); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/TileMapSpec/TileMapGraphicCircle.png'); - }); + drawWithTransform(engine.graphicsContext, tm, 99); + engine.graphicsContext.flush(); - it('should draw the correct proportions', async () => { - await texture.load(); - const tm = new ex.TileMap({ - pos: ex.vec(30, 30), - tileWidth: 64, - tileHeight: 48, - rows: 3, - columns: 7 - }); - const spriteTiles = ex.SpriteSheet.fromImageSource({ - image: texture, - grid: { - rows: 1, - columns: 1, - spriteWidth: 64, - spriteHeight: 48 - } - }); - tm.tiles.forEach(function (cell: ex.Tile) { - cell.solid = true; - cell.addGraphic(spriteTiles.sprites[0]); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/TileMapSpec/TileMapGraphicCircle.png'); }); - tm._initialize(engine); - drawWithTransform(engine.graphicsContext, tm, 100); - engine.graphicsContext.flush(); + it('should draw the correct proportions', async () => { + await texture.load(); + const tm = new ex.TileMap({ + pos: ex.vec(30, 30), + tileWidth: 64, + tileHeight: 48, + rows: 3, + columns: 7 + }); + const spriteTiles = ex.SpriteSheet.fromImageSource({ + image: texture, + grid: { + rows: 1, + columns: 1, + spriteWidth: 64, + spriteHeight: 48 + } + }); + tm.tiles.forEach(function (cell: ex.Tile) { + cell.solid = true; + cell.addGraphic(spriteTiles.sprites[0]); + }); + tm._initialize(engine); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/TileMapSpec/TileMap.png'); - }); + drawWithTransform(engine.graphicsContext, tm, 100); + engine.graphicsContext.flush(); - it('should draw from the bottom', async () => { - const tm = new ex.TileMap({ - pos: ex.vec(30, 30), - tileWidth: 64, - tileHeight: 48, - rows: 3, - columns: 7 - }); - const tileGraphic = new ex.Rectangle({ - color: ex.Color.Red, - strokeColor: ex.Color.Black, - width: 64, - height: 64 + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/TileMapSpec/TileMap.png'); }); - tm.tiles.forEach(function (cell: ex.Tile) { - cell.solid = true; - cell.addGraphic(tileGraphic); - }); - tm._initialize(engine); - drawWithTransform(engine.graphicsContext, tm, 100); - engine.graphicsContext.flush(); + it('should draw from the bottom', async () => { + const tm = new ex.TileMap({ + pos: ex.vec(30, 30), + tileWidth: 64, + tileHeight: 48, + rows: 3, + columns: 7 + }); + const tileGraphic = new ex.Rectangle({ + color: ex.Color.Red, + strokeColor: ex.Color.Black, + width: 64, + height: 64 + }); + tm.tiles.forEach(function (cell: ex.Tile) { + cell.solid = true; + cell.addGraphic(tileGraphic); + }); + tm._initialize(engine); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/TileMapSpec/tilemap-from-bottom.png'); - }); + drawWithTransform(engine.graphicsContext, tm, 100); + engine.graphicsContext.flush(); - it('should draw from the top', async () => { - const tm = new ex.TileMap({ - pos: ex.vec(30, 30), - tileWidth: 64, - tileHeight: 48, - rows: 3, - columns: 7, - renderFromTopOfGraphic: true + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/TileMapSpec/tilemap-from-bottom.png'); }); - const tileGraphic = new ex.Rectangle({ - color: ex.Color.Red, - strokeColor: ex.Color.Black, - width: 64, - height: 64 - }); - tm.tiles.forEach(function (cell: ex.Tile) { - cell.solid = true; - cell.addGraphic(tileGraphic); - }); - tm._initialize(engine); - drawWithTransform(engine.graphicsContext, tm, 100); - engine.graphicsContext.flush(); + it('should draw from the top', async () => { + const tm = new ex.TileMap({ + pos: ex.vec(30, 30), + tileWidth: 64, + tileHeight: 48, + rows: 3, + columns: 7, + renderFromTopOfGraphic: true + }); + const tileGraphic = new ex.Rectangle({ + color: ex.Color.Red, + strokeColor: ex.Color.Black, + width: 64, + height: 64 + }); + tm.tiles.forEach(function (cell: ex.Tile) { + cell.solid = true; + cell.addGraphic(tileGraphic); + }); + tm._initialize(engine); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/TileMapSpec/tilemap-from-top.png'); - }); + drawWithTransform(engine.graphicsContext, tm, 100); + engine.graphicsContext.flush(); - it('should handle offscreen culling correctly with negative coords', async () => { - await texture.load(); - const tm = new ex.TileMap({ - pos: ex.vec(-100, -100), - tileWidth: 64, - tileHeight: 48, - rows: 20, - columns: 20 - }); - const spriteTiles = ex.SpriteSheet.fromImageSource({ - image: texture, - grid: { - rows: 1, - columns: 1, - spriteWidth: 64, - spriteHeight: 48 - } + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/TileMapSpec/tilemap-from-top.png'); }); - tm.tiles.forEach(function (cell: ex.Tile) { - cell.solid = true; - cell.addGraphic(spriteTiles.sprites[0]); - }); - tm._initialize(engine); - tm.update(engine, 100); + it('should handle offscreen culling correctly with negative coords', async () => { + await texture.load(); + const tm = new ex.TileMap({ + pos: ex.vec(-100, -100), + tileWidth: 64, + tileHeight: 48, + rows: 20, + columns: 20 + }); + const spriteTiles = ex.SpriteSheet.fromImageSource({ + image: texture, + grid: { + rows: 1, + columns: 1, + spriteWidth: 64, + spriteHeight: 48 + } + }); + tm.tiles.forEach(function (cell: ex.Tile) { + cell.solid = true; + cell.addGraphic(spriteTiles.sprites[0]); + }); + tm._initialize(engine); - drawWithTransform(engine.graphicsContext, tm, 100); - engine.graphicsContext.flush(); + tm.update(engine, 100); - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/TileMapSpec/TileMapCulling.png'); - }); + drawWithTransform(engine.graphicsContext, tm, 100); + engine.graphicsContext.flush(); - it('should handle offscreen culling correctly when scaled', async () => { - await texture.load(); - const tm = new ex.TileMap({ - pos: ex.vec(100, 100), - tileWidth: 64, - tileHeight: 48, - rows: 20, - columns: 20 - }); - tm.scale = ex.vec(2, 2); - const spriteTiles = ex.SpriteSheet.fromImageSource({ - image: texture, - grid: { - rows: 1, - columns: 1, - spriteWidth: 64, - spriteHeight: 48 - } - }); - tm.tiles.forEach(function (cell: ex.Tile) { - cell.solid = true; - cell.addGraphic(spriteTiles.sprites[0]); + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/TileMapSpec/TileMapCulling.png'); }); - tm._initialize(engine); - engine.currentScene.add(tm); - - engine.currentScene.camera.x = 600; - engine.currentScene.update(engine, 100); - engine.currentScene.draw(engine.graphicsContext, 100); - engine.graphicsContext.flush(); - - expect(tm.getTile(0, 0).bounds).toEqual( - new ex.BoundingBox({ - left: 100, - top: 100, - right: 228, - bottom: 196 - }) - ); - expect(tm.getTile(1, 0).bounds).toEqual( - new ex.BoundingBox({ - left: 228, - top: 100, - right: 356, - bottom: 196 - }) - ); - - await expect(engine.canvas).toEqualImage('/src/spec/assets/images/TileMapSpec/tilemap-scaled.png'); + it('should handle offscreen culling correctly when scaled', async () => { + await texture.load(); + const tm = new ex.TileMap({ + pos: ex.vec(100, 100), + tileWidth: 64, + tileHeight: 48, + rows: 20, + columns: 20 + }); + tm.scale = ex.vec(2, 2); + const spriteTiles = ex.SpriteSheet.fromImageSource({ + image: texture, + grid: { + rows: 1, + columns: 1, + spriteWidth: 64, + spriteHeight: 48 + } + }); + tm.tiles.forEach(function (cell: ex.Tile) { + cell.solid = true; + cell.addGraphic(spriteTiles.sprites[0]); + }); + tm._initialize(engine); + engine.currentScene.add(tm); + + engine.currentScene.camera.x = 600; + engine.currentScene.update(engine, 100); + engine.currentScene.draw(engine.graphicsContext, 100); + engine.graphicsContext.flush(); + + expect(tm.getTile(0, 0).bounds).toEqual( + new ex.BoundingBox({ + left: 100, + top: 100, + right: 228, + bottom: 196 + }) + ); + + expect(tm.getTile(1, 0).bounds).toEqual( + new ex.BoundingBox({ + left: 228, + top: 100, + right: 356, + bottom: 196 + }) + ); + + await expect(engine.canvas).toEqualImage('/src/spec/assets/images/TileMapSpec/tilemap-scaled.png'); + }); }); it('can return a tile by xy coord', () => { diff --git a/src/spec/vitest/TiledAnimationSpec.ts b/src/spec/vitest/TiledAnimationSpec.ts index 1b508a74..1aaca6ae 100644 --- a/src/spec/vitest/TiledAnimationSpec.ts +++ b/src/spec/vitest/TiledAnimationSpec.ts @@ -21,48 +21,49 @@ describe('A TiledAnimation', () => { it('exists', () => { expect(ex.TiledAnimation).toBeDefined(); }); + describe('@visual', () => { + it('can be created', async () => { + const cardsImage = new ex.ImageSource('/src/spec/assets/images/TiledAnimationSpec/kenny-cards.png'); + await cardsImage.load(); + const cardSpriteSheet = ex.SpriteSheet.fromImageSource({ + image: cardsImage, + grid: { + rows: 4, + columns: 14, + spriteWidth: 42, + spriteHeight: 60 + }, + spacing: { + originOffset: { x: 11, y: 2 }, + margin: { x: 23, y: 5 } + } + }); - it('can be created', async () => { - const cardsImage = new ex.ImageSource('/src/spec/assets/images/TiledAnimationSpec/kenny-cards.png'); - await cardsImage.load(); - const cardSpriteSheet = ex.SpriteSheet.fromImageSource({ - image: cardsImage, - grid: { - rows: 4, - columns: 14, - spriteWidth: 42, - spriteHeight: 60 - }, - spacing: { - originOffset: { x: 11, y: 2 }, - margin: { x: 23, y: 5 } - } - }); - - cardSpriteSheet.sprites.forEach((s) => (s.scale = ex.vec(2, 2))); - const cardAnimation = ex.Animation.fromSpriteSheet(cardSpriteSheet, ex.range(0, 14 * 4), 200); + cardSpriteSheet.sprites.forEach((s) => (s.scale = ex.vec(2, 2))); + const cardAnimation = ex.Animation.fromSpriteSheet(cardSpriteSheet, ex.range(0, 14 * 4), 200); - const sut = new ex.TiledAnimation({ - animation: cardAnimation, - sourceView: { x: 20, y: 20 }, - width: 200, - height: 200, - wrapping: ex.ImageWrapping.Repeat - }); + const sut = new ex.TiledAnimation({ + animation: cardAnimation, + sourceView: { x: 20, y: 20 }, + width: 200, + height: 200, + wrapping: ex.ImageWrapping.Repeat + }); - await sut.ready; + await sut.ready; - ctx.clear(); - sut.draw(ctx, 20, 20); - ctx.flush(); + ctx.clear(); + sut.draw(ctx, 20, 20); + ctx.flush(); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/TiledAnimationSpec/tiled.png'); + await expect(canvasElement).toEqualImage('/src/spec/assets/images/TiledAnimationSpec/tiled.png'); - sut.tick(200); - ctx.clear(); - sut.draw(ctx, 20, 20); - ctx.flush(); + sut.tick(200); + ctx.clear(); + sut.draw(ctx, 20, 20); + ctx.flush(); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/TiledAnimationSpec/tiled-2.png'); + await expect(canvasElement).toEqualImage('/src/spec/assets/images/TiledAnimationSpec/tiled-2.png'); + }); }); }); diff --git a/src/spec/vitest/TiledSpriteSpec.ts b/src/spec/vitest/TiledSpriteSpec.ts index 65dce9ee..1e48325b 100644 --- a/src/spec/vitest/TiledSpriteSpec.ts +++ b/src/spec/vitest/TiledSpriteSpec.ts @@ -21,100 +21,101 @@ describe('A TiledSprite', () => { it('exists', () => { expect(ex.TiledSprite).toBeDefined(); }); - - it('can be created after load', async () => { - const image = new ex.ImageSource('/src/spec/assets/images/TiledSpriteSpec/ground.png'); - - await image.load(); - const sut = new ex.TiledSprite({ - image, - width: 400, - height: 400, - wrapping: { - x: ex.ImageWrapping.Repeat, - y: ex.ImageWrapping.Clamp - } + describe('@visual', () => { + it('can be created after load', async () => { + const image = new ex.ImageSource('/src/spec/assets/images/TiledSpriteSpec/ground.png'); + + await image.load(); + const sut = new ex.TiledSprite({ + image, + width: 400, + height: 400, + wrapping: { + x: ex.ImageWrapping.Repeat, + y: ex.ImageWrapping.Clamp + } + }); + await sut.ready; + + ctx.clear(); + sut.draw(ctx, 0, 20); + ctx.flush(); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/TiledSpriteSpec/tiled.png'); }); - await sut.ready; - - ctx.clear(); - sut.draw(ctx, 0, 20); - ctx.flush(); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/TiledSpriteSpec/tiled.png'); - }); - - it('can be created before load', async () => { - const image = new ex.ImageSource('/src/spec/assets/images/TiledSpriteSpec/ground.png'); - - const sut = new ex.TiledSprite({ - image, - width: 400, - height: 400, - wrapping: { - x: ex.ImageWrapping.Repeat, - y: ex.ImageWrapping.Clamp - } - }); - await image.load(); - await sut.ready; - - ctx.clear(); - sut.draw(ctx, 0, 20); - ctx.flush(); - await expect(canvasElement).toEqualImage('/src/spec/assets/images/TiledSpriteSpec/tiled.png'); - }); - - it('can be from a sprite', async () => { - const image = new ex.ImageSource('/src/spec/assets/images/TiledSpriteSpec/ground.png'); - const sourceSprite = image.toSprite(); - sourceSprite.width = 400; - sourceSprite.height = 400; - const sut = ex.TiledSprite.fromSprite(sourceSprite, { - wrapping: { - x: ex.ImageWrapping.Repeat, - y: ex.ImageWrapping.Clamp - } + it('can be created before load', async () => { + const image = new ex.ImageSource('/src/spec/assets/images/TiledSpriteSpec/ground.png'); + + const sut = new ex.TiledSprite({ + image, + width: 400, + height: 400, + wrapping: { + x: ex.ImageWrapping.Repeat, + y: ex.ImageWrapping.Clamp + } + }); + await image.load(); + await sut.ready; + + ctx.clear(); + sut.draw(ctx, 0, 20); + ctx.flush(); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/TiledSpriteSpec/tiled.png'); }); - await image.load(); - await sut.ready; - ctx.clear(); - sut.draw(ctx, 0, 20); - ctx.flush(); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/TiledSpriteSpec/tiled.png'); - }); - - it('can grab a tiled sprite from a sprite sheet', async () => { - const cardsImage = new ex.ImageSource('/src/spec/assets/images/TiledAnimationSpec/kenny-cards.png'); - await cardsImage.load(); - const cardSpriteSheet = ex.SpriteSheet.fromImageSource({ - image: cardsImage, - grid: { - rows: 4, - columns: 14, - spriteWidth: 42, - spriteHeight: 60 - }, - spacing: { - originOffset: { x: 11, y: 2 }, - margin: { x: 23, y: 5 } - } + it('can be from a sprite', async () => { + const image = new ex.ImageSource('/src/spec/assets/images/TiledSpriteSpec/ground.png'); + const sourceSprite = image.toSprite(); + sourceSprite.width = 400; + sourceSprite.height = 400; + const sut = ex.TiledSprite.fromSprite(sourceSprite, { + wrapping: { + x: ex.ImageWrapping.Repeat, + y: ex.ImageWrapping.Clamp + } + }); + await image.load(); + await sut.ready; + + ctx.clear(); + sut.draw(ctx, 0, 20); + ctx.flush(); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/TiledSpriteSpec/tiled.png'); }); - const sut = cardSpriteSheet.getTiledSprite(0, 0, { - width: 300, - height: 300, - scale: ex.vec(2, 2) + it('can grab a tiled sprite from a sprite sheet', async () => { + const cardsImage = new ex.ImageSource('/src/spec/assets/images/TiledAnimationSpec/kenny-cards.png'); + await cardsImage.load(); + const cardSpriteSheet = ex.SpriteSheet.fromImageSource({ + image: cardsImage, + grid: { + rows: 4, + columns: 14, + spriteWidth: 42, + spriteHeight: 60 + }, + spacing: { + originOffset: { x: 11, y: 2 }, + margin: { x: 23, y: 5 } + } + }); + + const sut = cardSpriteSheet.getTiledSprite(0, 0, { + width: 300, + height: 300, + scale: ex.vec(2, 2) + }); + await sut.ready; + + ctx.clear(); + sut.draw(ctx, 0, 0); + ctx.flush(); + + await expect(canvasElement).toEqualImage('/src/spec/assets/images/TiledSpriteSpec/from-spritesheet.png'); }); - await sut.ready; - - ctx.clear(); - sut.draw(ctx, 0, 0); - ctx.flush(); - - await expect(canvasElement).toEqualImage('/src/spec/assets/images/TiledSpriteSpec/from-spritesheet.png'); }); }); diff --git a/src/spec/vitest/TimerSpec.ts b/src/spec/vitest/TimerSpec.ts index 1c6ee345..fb2e2e23 100644 --- a/src/spec/vitest/TimerSpec.ts +++ b/src/spec/vitest/TimerSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('A Timer', () => { let timer; diff --git a/src/spec/vitest/TimescalingSpec.ts b/src/spec/vitest/TimescalingSpec.ts index 008d29c1..7bec830b 100644 --- a/src/spec/vitest/TimescalingSpec.ts +++ b/src/spec/vitest/TimescalingSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('The engine', () => { let engine: ex.Engine; diff --git a/src/spec/vitest/TransitionSpec.ts b/src/spec/vitest/TransitionSpec.ts index c827a212..252b5e3b 100644 --- a/src/spec/vitest/TransitionSpec.ts +++ b/src/spec/vitest/TransitionSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('A Transition', () => { it('exists', () => { diff --git a/src/spec/vitest/TriggerSpec.ts b/src/spec/vitest/TriggerSpec.ts index cfbff7f4..909158eb 100644 --- a/src/spec/vitest/TriggerSpec.ts +++ b/src/spec/vitest/TriggerSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { TestUtils } from './util/TestUtils'; +import { TestUtils } from '../__util__/TestUtils'; describe('A Trigger', () => { let scene: ex.Scene; diff --git a/src/spec/vitest/__setup__/matchers/convert.ts b/src/spec/vitest/__setup__/matchers/convert.ts deleted file mode 100644 index d5fbc852..00000000 --- a/src/spec/vitest/__setup__/matchers/convert.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* eslint-disable no-console */ -type ImageVisual = HTMLCanvasElement | CanvasRenderingContext2D | HTMLImageElement | string; -const convertSourceVisualToImageData = async (visual: ImageVisual): Promise => { - if (visual instanceof HTMLCanvasElement) { - return convertCanvas(visual); - } - if (visual instanceof CanvasRenderingContext2D) { - return convertContext(visual); - } - if (visual instanceof HTMLImageElement) { - return await convertImage(visual); - } - if (typeof visual === 'string') { - return await convertFilePath(visual); - } -}; - -const convertContext = (visual: CanvasRenderingContext2D): ImageData => { - return visual.getImageData(0, 0, visual.canvas.width, visual.canvas.height); -}; - -const convertCanvas = (canvas: HTMLCanvasElement): ImageData => { - const ctx = canvas.getContext('2d'); - const data = ctx.getImageData(0, 0, canvas.width, canvas.height); - return data; -}; - -const convertFilePath = async (imagePath: string, baseImagePath: string = ''): Promise => { - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - - const img = new Image(); - img.decoding = 'sync'; - if (imagePath) { - img.src = baseImagePath + imagePath + '?_=' + Math.random(); - try { - await img.decode(); - } catch { - console.warn(`Image could not be decoded, check image src: ${img.src}`); - } - } - - canvas.width = img.width; - canvas.height = img.height; - - ctx.drawImage(img, 0, 0); - return ctx.getImageData(0, 0, img.width, img.height); -}; - -const convertImage = async (image: HTMLImageElement): Promise => { - const canvas = document.createElement('canvas'); - image.decoding = 'sync'; - - if (image.src) { - try { - await image.decode(); - } catch { - console.warn(`Image could not be decoded, check image src: ${image.src}`); - } - } - - canvas.width = image.width; - canvas.height = image.height; - const ctx = canvas.getContext('2d'); - ctx.drawImage(image, 0, 0); - - return ctx.getImageData(0, 0, image.width, image.height); -}; - -export type { ImageVisual }; -export { convertSourceVisualToImageData }; diff --git a/src/spec/vitest/tsconfig.json b/src/spec/vitest/tsconfig.json index d16e66ca..ec463ebb 100644 --- a/src/spec/vitest/tsconfig.json +++ b/src/spec/vitest/tsconfig.json @@ -1,34 +1,4 @@ { - "compilerOptions": { - "types": ["@vitest/browser/providers/playwright", "vitest/globals"], - "moduleResolution": "node", - "sourceMap": true, - "experimentalDecorators": true, - "target": "es2015", - "module": "es2015", - "noImplicitAny": false, - "noUnusedLocals": false, - "downlevelIteration": true, - "allowSyntheticDefaultImports": true, - "lib": [ - "dom", - "dom.iterable", - "es5", - "es2015.collection", - "es2015.iterable", - "es2015.promise", - "es2015.proxy", - "es2018.promise", - "es2016", - "es2017.object", - "es2018" - ], - "baseUrl": ".", - "rootDir": "../../../", - "paths": { - "@excalibur": ["../../engine/"] - } - }, - "include": ["./**/*.ts", "./__setup__/**/*.ts"], - "files": ["../../engine/files.d.ts", "../../engine/globals.d.ts", "../../../vitest.config.ts"] + "extends": "../tsconfig.json", + "include": ["../vitest/**/*.ts", "../__util__/**/*.ts", "../__matchers__/**/*.ts", "../../../vitest.config.ts"] } diff --git a/vitest.config.ts b/vitest.config.ts index 62049bea..d85cdb14 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,26 +1,23 @@ /// -import * as os from 'os'; import * as path from 'path'; +import type { ViteUserConfig } from 'vitest/config'; import { defineConfig, mergeConfig } from 'vitest/config'; -import { EngineInstanceReporter } from './src/spec/vitest/__reporters__/engine-instance'; -import { MemoryReporter } from './src/spec/vitest/__reporters__/memory'; import commonConfig from './vite.config.common'; - -const isArmMacOS = process.platform === 'darwin' && process.arch === 'arm64'; -const HEADLESS = process.env.HEADLESS === 'true' || process.env.CI === 'true'; +import { EngineInstanceReporter } from './src/spec/__util__/reporters/engine-instance'; +import { MemoryReporter } from './src/spec/__util__/reporters/memory'; export default defineConfig( mergeConfig(commonConfig, { + // our image urls have always been relative from root in tests. would be nice to + // have a better place of where served files go for tests and then we just use /images/xyz.png + publicDir: __dirname, resolve: { alias: { '@excalibur': path.resolve(__dirname, './src/engine/') } }, test: { - globals: true, - setupFiles: ['./src/spec/vitest/__setup__/setup.ts'], - include: ['src/spec/vitest/**/*Spec.ts'], reporters: [['default', { summary: false }], new EngineInstanceReporter(), new MemoryReporter()], // enable with --coverage param coverage: { @@ -29,74 +26,9 @@ export default defineConfig( reporter: [['html'], ['lcov', { projectRoot: __dirname }], ['text-summary']], reportsDirectory: path.join(__dirname, 'coverage') }, - pool: 'threads', - browser: { - enabled: true, - provider: 'playwright', - // this will give each test their own environment. disabling this - // actually ended up breaking WebGL contexts in some cases - isolate: true, - - // this slows down the run but helps keep tests stable with how often - // we are opening and closing WebGL contexts - fileParallelism: false, - - headless: HEADLESS, - - // https://vitest.dev/guide/browser/playwright - // run `vitest --browser ` to run tests in a specific browser - instances: [ - { - browser: 'chromium', - provide: { - browser: 'chromium', - platform: os.platform() - }, - launch: { - channel: 'chrome', - ignoreDefaultArgs: ['--disable-render-backgrounding', '--disable-remote-fonts', '--font-render-hinting'], - args: [ - '--no-default-browser-check', - '--no-first-run', - '--disable-default-apps', - '--disable-popup-blocking', - '--disable-translate', - '--disable-background-timer-throttling', - !isArmMacOS && '--disable-gpu', - '--disable-dev-shm-usage', - - // on macOS, disable-background-timer-throttling is not enough - // and we need disable-renderer-backgrounding too - // see https://github.com/karma-runner/karma-chrome-launcher/issues/123 - '--disable-renderer-backgrounding', - '--disable-device-discovery-notifications', - - '--autoplay-policy=no-user-gesture-required', - '--mute-audio', - '--no-sandbox', - '--enable-precise-memory-info', - '--js-flags="--max_old_space_size=8192" --expose-gc' - ].filter(Boolean) - } - }, - { - browser: 'firefox', - provide: { - browser: 'firefox', - platform: os.platform() - } - }, - { - browser: 'webkit', - provide: { - browser: 'webkit', - platform: os.platform() - } - } - ] - } + projects: ['./src/spec/vitest.config.unit.ts', './src/spec/vitest.config.visual.ts'] } - }) + } satisfies ViteUserConfig) ); declare module 'vitest' {