From 26b9dc4c758ccd67225421358a0b9c10d17807a0 Mon Sep 17 00:00:00 2001 From: Guy Balaam Date: Thu, 22 Jan 2026 21:33:52 +0000 Subject: [PATCH 1/6] Adding screenshot testing --- packages/javascript/cypress.config.ts | 35 ++ .../cypress/component/SurfaceLayers.cy.ts | 20 ++ .../javascript/cypress/support/commands.ts | 68 ++-- packages/javascript/package.json | 3 +- yarn.lock | 332 +++++++++++++++++- 5 files changed, 428 insertions(+), 30 deletions(-) create mode 100644 packages/javascript/cypress/component/SurfaceLayers.cy.ts diff --git a/packages/javascript/cypress.config.ts b/packages/javascript/cypress.config.ts index c8e8d88..32f6251 100644 --- a/packages/javascript/cypress.config.ts +++ b/packages/javascript/cypress.config.ts @@ -1,8 +1,16 @@ import { defineConfig } from 'cypress'; +import z from 'zod'; +// eslint-disable-next-line @typescript-eslint/no-require-imports +const sharp = require('sharp'); export default defineConfig({ defaultBrowser: 'electron', component: { + setupNodeEvents(on) { + on('task', { + 'get-pixel-value': getPixelValue, + }); + }, devServer: { framework: 'react', bundler: 'vite', @@ -16,6 +24,7 @@ export default defineConfig({ setupNodeEvents(on) { // This is required to log progress to the terminal whilst generating frames on('task', { + 'get-pixel-value': getPixelValue, log(message) { console.log(message); return null; @@ -24,3 +33,29 @@ export default defineConfig({ }, }, }); + +const getPixelValueArgs = z.object({ + id: z.string(), + x: z.number().gte(0), + y: z.number().gte(0), +}); +async function getPixelValue(args: unknown) { + try { + const { id, x, y } = getPixelValueArgs.parse(args); + const file = `cypress/screenshots/${id}.png`; + + // Approach derived from: https://github.com/lovell/sharp/issues/934#issuecomment-327181099 + const metadata = await sharp(file).metadata(); + const { channels, width } = metadata; + const pixelOffset = channels * (width * y + x); + + const data = await sharp(file).raw().toBuffer(); + const r = data[pixelOffset + 0]; + const g = data[pixelOffset + 1]; + const b = data[pixelOffset + 2]; + + return { r, g, b }; + } catch (e) { + return e; + } +} diff --git a/packages/javascript/cypress/component/SurfaceLayers.cy.ts b/packages/javascript/cypress/component/SurfaceLayers.cy.ts new file mode 100644 index 0000000..88412a7 --- /dev/null +++ b/packages/javascript/cypress/component/SurfaceLayers.cy.ts @@ -0,0 +1,20 @@ +import { SurfaceManager } from '../../src/state-based/SurfaceManager'; + +describe('Surface layer tests', () => { + it('can take a known screenshot', () => { + const INDIANRED = { r: 191, g: 99, b: 96 }; + + const now = Date.now(); + const manager = new SurfaceManager({ + 'clip-id': { + file: 'cypress/fixtures/indianred@2560x1440.png', + type: 'image', + fit: 'cover', + keyframes: [[now, { set: { opacity: 1 } }]], + }, + }); + cy.mount(manager); + + cy.assertPixelAt(100, 100, INDIANRED); + }); +}); diff --git a/packages/javascript/cypress/support/commands.ts b/packages/javascript/cypress/support/commands.ts index 95857ae..29ba1cc 100644 --- a/packages/javascript/cypress/support/commands.ts +++ b/packages/javascript/cypress/support/commands.ts @@ -8,30 +8,44 @@ // commands please read more here: // https://on.cypress.io/custom-commands // *********************************************** -// -// -// -- This is a parent command -- -// Cypress.Commands.add('login', (email, password) => { ... }) -// -// -// -- This is a child command -- -// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This will overwrite an existing command -- -// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) -// -// declare global { -// namespace Cypress { -// interface Chainable { -// login(email: string, password: string): Chainable -// drag(subject: string, options?: Partial): Chainable -// dismiss(subject: string, options?: Partial): Chainable -// visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable -// } -// } -// } + +import { z } from 'zod'; +type Color = z.infer; +const rgb = z.object({ + r: z.int().gte(0).lte(255), + g: z.int().gte(0).lte(255), + b: z.int().gte(0).lte(255), +}); + +Cypress.Commands.add('getPixelAt', (x: number, y: number) => { + const id = '123'; + cy.screenshot(id, { overwrite: true, capture: 'viewport' }); + cy.task('get-pixel-value', { id, x, y }).then(($response) => { + return rgb.parse($response); + }); +}); + +// If all r,g,b are within ε of the expected value the color is expected +// An identical web page will be rendered differently depending on the color profile. +// This will be different on different machines, and displays +const RGB_ε = 15; +Cypress.Commands.add('assertPixelAt', (x: number, y: number, color: Color) => { + cy.getPixelAt(x, y).then(($response) => { + expect($response.r).to.be.closeTo(color.r, RGB_ε); + expect($response.g).to.be.closeTo(color.g, RGB_ε); + expect($response.b).to.be.closeTo(color.b, RGB_ε); + }); +}); + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + interface Chainable { + getPixelAt(x: number, y: number): Chainable; + assertPixelAt(x: number, y: number, color: Color): Chainable; + } + } +} + +// Required to declare global +export {}; diff --git a/packages/javascript/package.json b/packages/javascript/package.json index b74eef1..c3c849b 100644 --- a/packages/javascript/package.json +++ b/packages/javascript/package.json @@ -56,10 +56,11 @@ "prettier": "^3.4.2", "react": "^19.1.1", "react-dom": "^19.1.1", + "sharp": "^0.34.5", "typedoc": "^0.27.5", "typescript": "~5.7.2", "typescript-eslint": "^8.18.1", "vite": "^7.1.12", "vitest": "^4.0.6" } -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index bee3826..bdd541c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -90,6 +90,7 @@ __metadata: react: "npm:^19.1.1" react-dom: "npm:^19.1.1" reconnecting-websocket: "npm:^4.4.0" + sharp: "npm:^0.34.5" typedoc: "npm:^0.27.5" typescript: "npm:~5.7.2" typescript-eslint: "npm:^8.18.1" @@ -210,6 +211,15 @@ __metadata: languageName: node linkType: hard +"@emnapi/runtime@npm:^1.7.0": + version: 1.8.1 + resolution: "@emnapi/runtime@npm:1.8.1" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10c0/f4929d75e37aafb24da77d2f58816761fe3f826aad2e37fa6d4421dac9060cbd5098eea1ac3c9ecc4526b89deb58153852fa432f87021dc57863f2ff726d713f + languageName: node + linkType: hard + "@esbuild/aix-ppc64@npm:0.25.6": version: 0.25.6 resolution: "@esbuild/aix-ppc64@npm:0.25.6" @@ -614,6 +624,233 @@ __metadata: languageName: node linkType: hard +"@img/colour@npm:^1.0.0": + version: 1.0.0 + resolution: "@img/colour@npm:1.0.0" + checksum: 10c0/02261719c1e0d7aa5a2d585981954f2ac126f0c432400aa1a01b925aa2c41417b7695da8544ee04fd29eba7ecea8eaf9b8bef06f19dc8faba78f94eeac40667d + languageName: node + linkType: hard + +"@img/sharp-darwin-arm64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-darwin-arm64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-darwin-arm64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-darwin-arm64": + optional: true + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@img/sharp-darwin-x64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-darwin-x64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-darwin-x64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-darwin-x64": + optional: true + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@img/sharp-libvips-darwin-arm64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-darwin-arm64@npm:1.2.4" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@img/sharp-libvips-darwin-x64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-darwin-x64@npm:1.2.4" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-arm64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linux-arm64@npm:1.2.4" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-arm@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linux-arm@npm:1.2.4" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-ppc64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linux-ppc64@npm:1.2.4" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-riscv64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linux-riscv64@npm:1.2.4" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-s390x@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linux-s390x@npm:1.2.4" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-x64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linux-x64@npm:1.2.4" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linuxmusl-arm64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linuxmusl-arm64@npm:1.2.4" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-libvips-linuxmusl-x64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linuxmusl-x64@npm:1.2.4" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-linux-arm64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linux-arm64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linux-arm64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linux-arm64": + optional: true + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-arm@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linux-arm@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linux-arm": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linux-arm": + optional: true + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-ppc64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linux-ppc64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linux-ppc64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linux-ppc64": + optional: true + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-riscv64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linux-riscv64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linux-riscv64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linux-riscv64": + optional: true + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-s390x@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linux-s390x@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linux-s390x": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linux-s390x": + optional: true + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-x64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linux-x64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linux-x64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linux-x64": + optional: true + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linuxmusl-arm64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linuxmusl-arm64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linuxmusl-arm64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linuxmusl-arm64": + optional: true + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-linuxmusl-x64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linuxmusl-x64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linuxmusl-x64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linuxmusl-x64": + optional: true + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-wasm32@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-wasm32@npm:0.34.5" + dependencies: + "@emnapi/runtime": "npm:^1.7.0" + conditions: cpu=wasm32 + languageName: node + linkType: hard + +"@img/sharp-win32-arm64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-win32-arm64@npm:0.34.5" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@img/sharp-win32-ia32@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-win32-ia32@npm:0.34.5" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@img/sharp-win32-x64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-win32-x64@npm:0.34.5" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -2295,6 +2532,13 @@ __metadata: languageName: node linkType: hard +"detect-libc@npm:^2.1.2": + version: 2.1.2 + resolution: "detect-libc@npm:2.1.2" + checksum: 10c0/acc675c29a5649fa1fb6e255f993b8ee829e510b6b56b0910666949c80c364738833417d0edb5f90e4e46be17228b0f2b66a010513984e18b15deeeac49369c4 + languageName: node + linkType: hard + "doctrine@npm:^2.1.0": version: 2.1.0 resolution: "doctrine@npm:2.1.0" @@ -5407,7 +5651,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.7.1": +"semver@npm:^7.7.1, semver@npm:^7.7.3": version: 7.7.3 resolution: "semver@npm:7.7.3" bin: @@ -5442,6 +5686,90 @@ __metadata: languageName: node linkType: hard +"sharp@npm:^0.34.5": + version: 0.34.5 + resolution: "sharp@npm:0.34.5" + dependencies: + "@img/colour": "npm:^1.0.0" + "@img/sharp-darwin-arm64": "npm:0.34.5" + "@img/sharp-darwin-x64": "npm:0.34.5" + "@img/sharp-libvips-darwin-arm64": "npm:1.2.4" + "@img/sharp-libvips-darwin-x64": "npm:1.2.4" + "@img/sharp-libvips-linux-arm": "npm:1.2.4" + "@img/sharp-libvips-linux-arm64": "npm:1.2.4" + "@img/sharp-libvips-linux-ppc64": "npm:1.2.4" + "@img/sharp-libvips-linux-riscv64": "npm:1.2.4" + "@img/sharp-libvips-linux-s390x": "npm:1.2.4" + "@img/sharp-libvips-linux-x64": "npm:1.2.4" + "@img/sharp-libvips-linuxmusl-arm64": "npm:1.2.4" + "@img/sharp-libvips-linuxmusl-x64": "npm:1.2.4" + "@img/sharp-linux-arm": "npm:0.34.5" + "@img/sharp-linux-arm64": "npm:0.34.5" + "@img/sharp-linux-ppc64": "npm:0.34.5" + "@img/sharp-linux-riscv64": "npm:0.34.5" + "@img/sharp-linux-s390x": "npm:0.34.5" + "@img/sharp-linux-x64": "npm:0.34.5" + "@img/sharp-linuxmusl-arm64": "npm:0.34.5" + "@img/sharp-linuxmusl-x64": "npm:0.34.5" + "@img/sharp-wasm32": "npm:0.34.5" + "@img/sharp-win32-arm64": "npm:0.34.5" + "@img/sharp-win32-ia32": "npm:0.34.5" + "@img/sharp-win32-x64": "npm:0.34.5" + detect-libc: "npm:^2.1.2" + semver: "npm:^7.7.3" + dependenciesMeta: + "@img/sharp-darwin-arm64": + optional: true + "@img/sharp-darwin-x64": + optional: true + "@img/sharp-libvips-darwin-arm64": + optional: true + "@img/sharp-libvips-darwin-x64": + optional: true + "@img/sharp-libvips-linux-arm": + optional: true + "@img/sharp-libvips-linux-arm64": + optional: true + "@img/sharp-libvips-linux-ppc64": + optional: true + "@img/sharp-libvips-linux-riscv64": + optional: true + "@img/sharp-libvips-linux-s390x": + optional: true + "@img/sharp-libvips-linux-x64": + optional: true + "@img/sharp-libvips-linuxmusl-arm64": + optional: true + "@img/sharp-libvips-linuxmusl-x64": + optional: true + "@img/sharp-linux-arm": + optional: true + "@img/sharp-linux-arm64": + optional: true + "@img/sharp-linux-ppc64": + optional: true + "@img/sharp-linux-riscv64": + optional: true + "@img/sharp-linux-s390x": + optional: true + "@img/sharp-linux-x64": + optional: true + "@img/sharp-linuxmusl-arm64": + optional: true + "@img/sharp-linuxmusl-x64": + optional: true + "@img/sharp-wasm32": + optional: true + "@img/sharp-win32-arm64": + optional: true + "@img/sharp-win32-ia32": + optional: true + "@img/sharp-win32-x64": + optional: true + checksum: 10c0/fd79e29df0597a7d5704b8461c51f944ead91a5243691697be6e8243b966402beda53ddc6f0a53b96ea3cb8221f0b244aa588114d3ebf8734fb4aefd41ab802f + languageName: node + linkType: hard + "shebang-command@npm:^2.0.0": version: 2.0.0 resolution: "shebang-command@npm:2.0.0" @@ -6044,7 +6372,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.1.0, tslib@npm:^2.6.2": +"tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.6.2": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 From ae38a4411c2279610cff3d967c65904b09adffe1 Mon Sep 17 00:00:00 2001 From: Guy Balaam Date: Thu, 22 Jan 2026 22:54:54 +0000 Subject: [PATCH 2/6] Flatening screenshot files --- packages/javascript/cypress.config.ts | 26 ++++++++++++++++--- .../javascript/cypress/support/commands.ts | 7 ++++- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/javascript/cypress.config.ts b/packages/javascript/cypress.config.ts index 32f6251..0b4939d 100644 --- a/packages/javascript/cypress.config.ts +++ b/packages/javascript/cypress.config.ts @@ -3,10 +3,14 @@ import z from 'zod'; // eslint-disable-next-line @typescript-eslint/no-require-imports const sharp = require('sharp'); +import { rename } from 'node:fs/promises'; + export default defineConfig({ defaultBrowser: 'electron', component: { setupNodeEvents(on) { + // + on('after:screenshot', flattenScreenshots); on('task', { 'get-pixel-value': getPixelValue, }); @@ -22,6 +26,8 @@ export default defineConfig({ e2e: { setupNodeEvents(on) { + // + on('after:screenshot', flattenScreenshots); // This is required to log progress to the terminal whilst generating frames on('task', { 'get-pixel-value': getPixelValue, @@ -41,21 +47,35 @@ const getPixelValueArgs = z.object({ }); async function getPixelValue(args: unknown) { try { + console.debug('getPixelValue()', args); const { id, x, y } = getPixelValueArgs.parse(args); - const file = `cypress/screenshots/${id}.png`; + const path = `cypress/screenshots/${id}.png`; // Approach derived from: https://github.com/lovell/sharp/issues/934#issuecomment-327181099 - const metadata = await sharp(file).metadata(); + const metadata = await sharp(path).metadata(); const { channels, width } = metadata; const pixelOffset = channels * (width * y + x); - const data = await sharp(file).raw().toBuffer(); + const data = await sharp(path).raw().toBuffer(); const r = data[pixelOffset + 0]; const g = data[pixelOffset + 1]; const b = data[pixelOffset + 2]; return { r, g, b }; } catch (e) { + console.error(e); return e; } } + +// If cypress is in interactive mode screenshots are saved in /screenshots/{name}.png +// If cypress is in run mode, screenshots are saved in /screenshots/{test-name}/{name}.png +const CYPRESS_SCREENSHOTS = 'cypress/screenshots'; +async function flattenScreenshots(details: { path: string; specName: string }) { + if (details.specName.length) { + const newPath = details.path.replace(`${CYPRESS_SCREENSHOTS}/${details.specName}`, CYPRESS_SCREENSHOTS); + await rename(details.path, newPath); + return { path: newPath }; + } + return details; +} diff --git a/packages/javascript/cypress/support/commands.ts b/packages/javascript/cypress/support/commands.ts index 29ba1cc..e0530f4 100644 --- a/packages/javascript/cypress/support/commands.ts +++ b/packages/javascript/cypress/support/commands.ts @@ -21,7 +21,12 @@ Cypress.Commands.add('getPixelAt', (x: number, y: number) => { const id = '123'; cy.screenshot(id, { overwrite: true, capture: 'viewport' }); cy.task('get-pixel-value', { id, x, y }).then(($response) => { - return rgb.parse($response); + try { + return rgb.parse($response); + } catch (e) { + console.error($response); + console.error(e); + } }); }); From db1f3d0443ec5bf9c54953ffca81e9b89dd09a04 Mon Sep 17 00:00:00 2001 From: Guy Balaam Date: Thu, 22 Jan 2026 23:05:38 +0000 Subject: [PATCH 3/6] Improving error messaging --- packages/javascript/cypress.config.ts | 4 +--- packages/javascript/cypress/support/commands.ts | 12 +++++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/javascript/cypress.config.ts b/packages/javascript/cypress.config.ts index 0b4939d..aebe532 100644 --- a/packages/javascript/cypress.config.ts +++ b/packages/javascript/cypress.config.ts @@ -9,7 +9,6 @@ export default defineConfig({ defaultBrowser: 'electron', component: { setupNodeEvents(on) { - // on('after:screenshot', flattenScreenshots); on('task', { 'get-pixel-value': getPixelValue, @@ -26,11 +25,10 @@ export default defineConfig({ e2e: { setupNodeEvents(on) { - // on('after:screenshot', flattenScreenshots); - // This is required to log progress to the terminal whilst generating frames on('task', { 'get-pixel-value': getPixelValue, + // This is required to log progress to the terminal whilst generating frames log(message) { console.log(message); return null; diff --git a/packages/javascript/cypress/support/commands.ts b/packages/javascript/cypress/support/commands.ts index e0530f4..ff1d3e9 100644 --- a/packages/javascript/cypress/support/commands.ts +++ b/packages/javascript/cypress/support/commands.ts @@ -34,11 +34,13 @@ Cypress.Commands.add('getPixelAt', (x: number, y: number) => { // An identical web page will be rendered differently depending on the color profile. // This will be different on different machines, and displays const RGB_ε = 15; -Cypress.Commands.add('assertPixelAt', (x: number, y: number, color: Color) => { - cy.getPixelAt(x, y).then(($response) => { - expect($response.r).to.be.closeTo(color.r, RGB_ε); - expect($response.g).to.be.closeTo(color.g, RGB_ε); - expect($response.b).to.be.closeTo(color.b, RGB_ε); +Cypress.Commands.add('assertPixelAt', (x: number, y: number, expected: Color) => { + cy.getPixelAt(x, y).then(($actual) => { + const expectedColor = `{r:${expected.r},g:${expected.g},b:${expected.b}}`; + const actualColor = `{r:${$actual.r},g:${$actual.g},b:${$actual.b}}`; + expect($actual.r).to.be.closeTo(expected.r, RGB_ε, `Expected within ${RGB_ε} of color ${expectedColor}, but got ${actualColor}`); + expect($actual.g).to.be.closeTo(expected.g, RGB_ε, `Expected within ${RGB_ε} of color ${expectedColor}, but got ${actualColor}`); + expect($actual.b).to.be.closeTo(expected.b, RGB_ε, `Expected within ${RGB_ε} of color ${expectedColor}, but got ${actualColor}`); }); }); From 0c8193c8becaffa1e5c1f699823bb72d9529c845 Mon Sep 17 00:00:00 2001 From: Guy Balaam Date: Fri, 23 Jan 2026 09:36:28 +0000 Subject: [PATCH 4/6] Ensuring image has loaded --- packages/javascript/cypress/component/SurfaceLayers.cy.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/javascript/cypress/component/SurfaceLayers.cy.ts b/packages/javascript/cypress/component/SurfaceLayers.cy.ts index 88412a7..a7bf486 100644 --- a/packages/javascript/cypress/component/SurfaceLayers.cy.ts +++ b/packages/javascript/cypress/component/SurfaceLayers.cy.ts @@ -15,6 +15,13 @@ describe('Surface layer tests', () => { }); cy.mount(manager); + // Wait for image to load (naturalWidth is set once loaded) + cy.get('img') + .should('be.visible') + .and(($img) => { + expect($img[0].naturalWidth).to.be.greaterThan(0); + }); + cy.assertPixelAt(100, 100, INDIANRED); }); }); From 2a4e6911dda688873e2da85043b2ad301a703feb Mon Sep 17 00:00:00 2001 From: Guy Balaam Date: Fri, 23 Jan 2026 12:12:48 +0000 Subject: [PATCH 5/6] Adding zIndex --- .../cypress/component/SurfaceLayers.cy.ts | 42 ++++++++++++++++-- .../cypress/fixtures/royalblue@2560x1440.png | Bin 0 -> 16872 bytes .../javascript/cypress/support/commands.ts | 2 +- .../src/state-based/ImageManager.ts | 9 +++- .../src/state-based/VideoManager.ts | 7 +++ packages/javascript/src/types/MediaSchema.ts | 3 ++ 6 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 packages/javascript/cypress/fixtures/royalblue@2560x1440.png diff --git a/packages/javascript/cypress/component/SurfaceLayers.cy.ts b/packages/javascript/cypress/component/SurfaceLayers.cy.ts index a7bf486..7bf2309 100644 --- a/packages/javascript/cypress/component/SurfaceLayers.cy.ts +++ b/packages/javascript/cypress/component/SurfaceLayers.cy.ts @@ -1,12 +1,12 @@ import { SurfaceManager } from '../../src/state-based/SurfaceManager'; +const INDIAN_RED = { r: 191, g: 99, b: 96 }; +const ROYAL_BLUE = { r: 75, g: 104, b: 218 }; describe('Surface layer tests', () => { it('can take a known screenshot', () => { - const INDIANRED = { r: 191, g: 99, b: 96 }; - const now = Date.now(); const manager = new SurfaceManager({ - 'clip-id': { + red: { file: 'cypress/fixtures/indianred@2560x1440.png', type: 'image', fit: 'cover', @@ -22,6 +22,40 @@ describe('Surface layer tests', () => { expect($img[0].naturalWidth).to.be.greaterThan(0); }); - cy.assertPixelAt(100, 100, INDIANRED); + cy.assertPixelAt(100, 100, INDIAN_RED); + }); + + it.only('respects z-index', () => { + const now = Date.now(); + const manager = new SurfaceManager({ + red: { + file: 'cypress/fixtures/indianred@2560x1440.png', + type: 'image', + fit: 'cover', + keyframes: [ + [now, { set: { zIndex: 100 } }], + [now + 2000, { set: { zIndex: 300 } }], + ], + }, + blue: { + file: 'cypress/fixtures/royalblue@2560x1440.png', + type: 'image', + fit: 'cover', + keyframes: [[now, { set: { zIndex: 200 } }]], + }, + }); + cy.mount(manager); + + // Wait for images to load (naturalWidth is set once loaded) + cy.get('img') + .should('be.visible') + .and(($img) => { + expect($img[0].naturalWidth).to.be.greaterThan(0); + expect($img[1].naturalWidth).to.be.greaterThan(0); + }); + + cy.assertPixelAt(100, 100, ROYAL_BLUE); + cy.wait(2000); + cy.assertPixelAt(100, 100, INDIAN_RED); }); }); diff --git a/packages/javascript/cypress/fixtures/royalblue@2560x1440.png b/packages/javascript/cypress/fixtures/royalblue@2560x1440.png new file mode 100644 index 0000000000000000000000000000000000000000..98e433d9efd60826c3325e59847caa30d27201e4 GIT binary patch literal 16872 zcmeI2F)u@56oyZgrjgvm*r*su3>_?DXi23SNz;g(*eufMK#1gGVj-puNH-e>gBYY1 z3nF4`Buu(sFdA^1_MCfvfuuZl=^2`@U!T19d(V6B-OT4QPG_uBM4aq+dQzlbc9f$L z?_*bDPa-47+4RWNQt0(EHAEc$Jdqv)0+3dv}WH=RA+%{&7wc8 zc@9i#9s`}`us^LSu(+zn1&g8()M6;Oi428swipYJTtiX8iHrpXZ766h3-uWWs7&u7ZsM5lR?qLhS?q6`Ho%6};h z2QXRS0EVLF9DtoS2@rHl3IrX3j*0=BQxXIn9?z;4FbgFim<48`V!$kv#BZ8KrMeOB z@_x8Ib^m?;7M;>PkVSZ+qgr5|rzDu?@qV6afx(rM5X=IzP%&T@N&>UMEL02s$1EbJ VHRt+ncA!}Q>)Ekf`e<}!{Rad0M;8D9 literal 0 HcmV?d00001 diff --git a/packages/javascript/cypress/support/commands.ts b/packages/javascript/cypress/support/commands.ts index ff1d3e9..fe9a58a 100644 --- a/packages/javascript/cypress/support/commands.ts +++ b/packages/javascript/cypress/support/commands.ts @@ -18,7 +18,7 @@ const rgb = z.object({ }); Cypress.Commands.add('getPixelAt', (x: number, y: number) => { - const id = '123'; + const id = Math.random().toString(16).slice(2); cy.screenshot(id, { overwrite: true, capture: 'viewport' }); cy.task('get-pixel-value', { id, x, y }).then(($response) => { try { diff --git a/packages/javascript/src/state-based/ImageManager.ts b/packages/javascript/src/state-based/ImageManager.ts index 7f75ea5..3784553 100644 --- a/packages/javascript/src/state-based/ImageManager.ts +++ b/packages/javascript/src/state-based/ImageManager.ts @@ -1,4 +1,4 @@ -import { ImageState } from '../types/MediaSchema'; +import { defaultImageOptions, ImageState } from '../types/MediaSchema'; import { getStateAtTime } from '../utils/getStateAtTime'; import { ClipManager } from './ClipManager'; @@ -39,6 +39,13 @@ export class ImageManager extends ClipManager { if (this.imageElement.style.objectFit !== this._state.fit) { this.imageElement.style.objectFit = this._state.fit; } + if (parseFloat(this.imageElement.style.opacity) !== currentState.opacity) { + this.imageElement.style.opacity = String(currentState.opacity ?? defaultImageOptions.opacity); + } + const z = Math.round(currentState.zIndex ?? defaultImageOptions.zIndex); + if (parseInt(this.imageElement.style.zIndex) !== z) { + this.imageElement.style.zIndex = String(z); + } const { opacity } = currentState; if (typeof opacity === 'string' && opacity !== this.imageElement.style.opacity) { diff --git a/packages/javascript/src/state-based/VideoManager.ts b/packages/javascript/src/state-based/VideoManager.ts index 5fbfdf5..0b95543 100644 --- a/packages/javascript/src/state-based/VideoManager.ts +++ b/packages/javascript/src/state-based/VideoManager.ts @@ -73,6 +73,13 @@ export class VideoManager extends ClipManager { if (this.videoElement.style.objectFit !== this._state.fit) { this.videoElement.style.objectFit = this._state.fit; } + if (parseFloat(this.videoElement.style.opacity) !== currentState.opacity) { + this.videoElement.style.opacity = String(currentState.opacity ?? defaultVideoOptions.opacity); + } + const z = Math.round(currentState.zIndex ?? defaultVideoOptions.zIndex); + if (parseInt(this.videoElement.style.zIndex) !== z) { + this.videoElement.style.zIndex = String(z); + } if (this.videoElement.volume !== volume) { this.videoElement.volume = volume; } diff --git a/packages/javascript/src/types/MediaSchema.ts b/packages/javascript/src/types/MediaSchema.ts index 4d12bb0..f3c00fc 100644 --- a/packages/javascript/src/types/MediaSchema.ts +++ b/packages/javascript/src/types/MediaSchema.ts @@ -8,6 +8,7 @@ const TemporalProperties = z.object({ export type VisualProperties = z.infer; const VisualProperties = z.object({ opacity: z.number().gte(0).lte(1), + zIndex: z.number(), }); export type AudialProperties = z.infer; const AudialProperties = z.object({ @@ -213,6 +214,7 @@ true satisfies UnionsEqual Date: Tue, 27 Jan 2026 14:24:55 +0000 Subject: [PATCH 6/6] Re-enabling tests --- packages/javascript/cypress/component/SurfaceLayers.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/javascript/cypress/component/SurfaceLayers.cy.ts b/packages/javascript/cypress/component/SurfaceLayers.cy.ts index 7bf2309..dbc5942 100644 --- a/packages/javascript/cypress/component/SurfaceLayers.cy.ts +++ b/packages/javascript/cypress/component/SurfaceLayers.cy.ts @@ -25,7 +25,7 @@ describe('Surface layer tests', () => { cy.assertPixelAt(100, 100, INDIAN_RED); }); - it.only('respects z-index', () => { + it('respects z-index', () => { const now = Date.now(); const manager = new SurfaceManager({ red: {