From 26f45da270f994290de90a3262ba4e8cd32b44fc Mon Sep 17 00:00:00 2001 From: Matt Jennings Date: Tue, 2 Sep 2025 08:44:42 -0500 Subject: [PATCH] chore: split out vitest tests into visual & unit (#3508) This splits out our vitest test suite into `unit` and `visual` tests. `unit` will run on chromium, while `visual` will run on chrome. I've removed firefox/webkit browsers as we were not running these in CI, and there's a discussion to be had about whether they even make sense to use in unit tests, or are they actually only valuable for visual tests. If the latter, we need to figure out how to not rely on Chrome for consistent screenshotting. Tests that are visual simply need to contain `@visual` somewhere in it's test name or `describe` name. Unit tests will run for all tests without `@visual`, while visual tests will run all that contain `@visual`. I had considered splitting them out into different files under different folders, but it's nice to re-use the same setup/teardown logic. Otherwise I had to copy & paste all of that in both places. I also made some other tweaks to our vitest config: - I've removed the conditional exemption of `--disable-gpu` flag for Chromium on macOS, which although slower it ensures the visual tests pass. I suppose this is because it uses a software renderer. - Changed the test pooling from `thread` to the default. I never saw a difference in speed but in theory the default (`fork`) is supposed to be faster - Enabled fileParallelism for tests. I had run into WebGL context issues before, but now they seem to be gone, so this should lead to faster tests - I've switched from Chrome to actual Chromium for the visual tests, and then lowered the tolerance on the TextSpec so that it could pass both in Windows/Ubuntu CI as well as macOS locally. I'm fine to undo this, but a lower tolerance on font tests might make sense to do anyway and this frees us from relying on Windows+Chrome to run these tests. --- .github/workflows/build.yml | 4 +- CHANGELOG.md | 1 + package.json | 9 +- src/engine/Graphics/Font.ts | 5 +- src/spec/__matchers__/expect.ts | 81 + src/spec/__matchers__/expect.visual.ts | 24 + .../image-helpers.ts} | 155 +- src/spec/{vitest/util => __util__}/Mocks.ts | 0 .../{vitest/util => __util__}/TestUtils.ts | 0 .../reporters}/engine-instance.setup.ts | 0 .../reporters}/engine-instance.ts | 0 .../reporters}/memory.setup.ts | 0 .../reporters}/memory.ts | 0 .../{vitest/__setup__ => __util__}/setup.ts | 5 +- src/spec/tsconfig.json | 34 + src/spec/vitest.config.unit.ts | 43 + src/spec/vitest.config.visual.ts | 68 + src/spec/vitest/ActionSpec.ts | 2 +- src/spec/vitest/ActorSpec.ts | 6 +- src/spec/vitest/AnimationSpec.ts | 2 +- src/spec/vitest/ArcadeSolverSpec.ts | 2 +- src/spec/vitest/CameraSpec.ts | 2 +- src/spec/vitest/CanvasSpec.ts | 6 +- src/spec/vitest/CircleSpec.ts | 4 +- src/spec/vitest/CollisionShapeSpec.ts | 14 +- src/spec/vitest/CollisionSpec.ts | 2 +- src/spec/vitest/ColorBlindCorrectorSpec.ts | 234 +-- src/spec/vitest/CompositeColliderSpec.ts | 2 +- src/spec/vitest/CoroutineSpec.ts | 2 +- src/spec/vitest/CrossFadeSpec.ts | 4 +- src/spec/vitest/CurveActionSpec.ts | 2 +- src/spec/vitest/DebugSpec.ts | 250 +-- src/spec/vitest/DebugSystemSpec.ts | 316 +-- src/spec/vitest/DebugTextSpec.ts | 48 +- src/spec/vitest/DefaultLoaderSpec.ts | 4 +- src/spec/vitest/DirectorSpec.ts | 4 +- src/spec/vitest/EngineSpec.ts | 392 ++-- .../vitest/ExcaliburGraphicsContextSpec.ts | 1840 +++++++++-------- src/spec/vitest/FadeInOutSpec.ts | 6 +- src/spec/vitest/FrameStatsSpec.ts | 4 +- src/spec/vitest/GamepadSpec.ts | 4 +- src/spec/vitest/GifSpec.ts | 134 +- src/spec/vitest/GpuParticleSpec.ts | 354 ++-- src/spec/vitest/GraphicSpec.ts | 104 +- src/spec/vitest/GraphicsGroupSpec.ts | 138 +- src/spec/vitest/GraphicsSystemSpec.ts | 464 ++--- src/spec/vitest/ImageSourceSpec.ts | 98 +- src/spec/vitest/InputMapperSpec.ts | 2 +- src/spec/vitest/IsometricMapSpec.ts | 208 +- src/spec/vitest/KeyInputSpec.ts | 2 +- src/spec/vitest/LabelSpec.ts | 2 +- .../LineSegmentSpec.ts} | 2 +- src/spec/vitest/LineSpec.ts | 84 +- src/spec/vitest/LoaderSpec.ts | 60 +- src/spec/vitest/MaterialRendererSpec.ts | 548 ++--- src/spec/vitest/NineSliceSpec.ts | 88 +- src/spec/vitest/OffscreenSystemSpec.ts | 2 +- src/spec/vitest/ParallaxSpec.ts | 142 +- src/spec/vitest/ParticleSpec.ts | 254 +-- src/spec/vitest/PhysicsWorldSpec.ts | 2 +- src/spec/vitest/PointerInputSpec.ts | 2 +- src/spec/vitest/PolygonSpec.ts | 18 +- src/spec/{ => vitest}/ProjectionsSpec.ts | 2 +- src/spec/vitest/RasterSpec.ts | 34 +- src/spec/vitest/RectangleSpec.ts | 60 +- src/spec/vitest/ResourceSpec.ts | 2 +- src/spec/vitest/ScaleSpec.ts | 59 +- src/spec/vitest/SceneSpec.ts | 2 +- src/spec/vitest/ScreenAppenderSpec.ts | 46 +- src/spec/vitest/ScreenElementSpec.ts | 30 +- src/spec/vitest/SlideSpec.ts | 389 ++-- src/spec/vitest/SoundSpec.ts | 2 +- src/spec/vitest/SpriteSheetSpec.ts | 104 +- src/spec/vitest/SpriteSpec.ts | 368 ++-- src/spec/vitest/TextSpec.ts | 1373 ++++++------ src/spec/vitest/TileMapSpec.ts | 370 ++-- src/spec/vitest/TiledAnimationSpec.ts | 73 +- src/spec/vitest/TiledSpriteSpec.ts | 177 +- src/spec/vitest/TimerSpec.ts | 2 +- src/spec/vitest/TimescalingSpec.ts | 2 +- src/spec/vitest/TransitionSpec.ts | 2 +- src/spec/vitest/TriggerSpec.ts | 2 +- src/spec/vitest/__setup__/matchers/convert.ts | 71 - src/spec/vitest/tsconfig.json | 34 +- vitest.config.ts | 84 +- 85 files changed, 4860 insertions(+), 4717 deletions(-) create mode 100644 src/spec/__matchers__/expect.ts create mode 100644 src/spec/__matchers__/expect.visual.ts rename src/spec/{vitest/__setup__/matchers/expect.ts => __matchers__/image-helpers.ts} (60%) rename src/spec/{vitest/util => __util__}/Mocks.ts (100%) rename src/spec/{vitest/util => __util__}/TestUtils.ts (100%) rename src/spec/{vitest/__reporters__ => __util__/reporters}/engine-instance.setup.ts (100%) rename src/spec/{vitest/__reporters__ => __util__/reporters}/engine-instance.ts (100%) rename src/spec/{vitest/__reporters__ => __util__/reporters}/memory.setup.ts (100%) rename src/spec/{vitest/__reporters__ => __util__/reporters}/memory.ts (100%) rename src/spec/{vitest/__setup__ => __util__}/setup.ts (64%) create mode 100644 src/spec/tsconfig.json create mode 100644 src/spec/vitest.config.unit.ts create mode 100644 src/spec/vitest.config.visual.ts rename src/spec/{LinesSpec.ts => vitest/LineSegmentSpec.ts} (99%) rename src/spec/{ => vitest}/ProjectionsSpec.ts (96%) delete mode 100644 src/spec/vitest/__setup__/matchers/convert.ts 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' { -- 2.51.2